mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[api] add creator filter to clothing search endpoint
This commit is contained in:
@@ -413,6 +413,13 @@ export interface CustomAvatarItemSearch {
|
|||||||
* between them, never as a superset.
|
* between them, never as a superset.
|
||||||
*/
|
*/
|
||||||
includeCoachItems?: boolean
|
includeCoachItems?: boolean
|
||||||
|
/**
|
||||||
|
* Only items this account authored — how the client lists what one player has made (a
|
||||||
|
* profile's creations, `creatorAccountId=<id>&includeCoachItems=False&take=1000`). Combines
|
||||||
|
* with the rest as one more AND; naming the Coach here with `includeCoachItems=false`
|
||||||
|
* matches nothing, as the two conditions read.
|
||||||
|
*/
|
||||||
|
creatorAccountId?: number
|
||||||
/** Lowest price to include, inclusive. */
|
/** Lowest price to include, inclusive. */
|
||||||
minPrice?: number
|
minPrice?: number
|
||||||
/** Highest price to include, inclusive. */
|
/** Highest price to include, inclusive. */
|
||||||
@@ -447,6 +454,9 @@ export const SEARCH_MAX_TAKE = 200
|
|||||||
* `True` and its user-generated-content tab sends `False`, and the two must not overlap — read
|
* `True` and its user-generated-content tab sends `False`, and the two must not overlap — read
|
||||||
* as "stock plus players'", the storefront would show every player's item too.
|
* as "stock plus players'", the storefront would show every player's item too.
|
||||||
*
|
*
|
||||||
|
* `creatorAccountId` narrows to one author. It was ignored until it was noticed that a player's
|
||||||
|
* creations list (which sends it) was serving the whole player-made catalog under every name.
|
||||||
|
*
|
||||||
* Ordered by recency because there is nothing else to order by — no purchase counts, no wear
|
* Ordered by recency because there is nothing else to order by — no purchase counts, no wear
|
||||||
* counts, no ratings are recorded — which is the same stand-in the `hot` feed makes. The
|
* counts, no ratings are recorded — which is the same stand-in the `hot` feed makes. The
|
||||||
* `custom_avatar_item_id` tiebreak is what makes paging stable: without it, two items sharing a
|
* `custom_avatar_item_id` tiebreak is what makes paging stable: without it, two items sharing a
|
||||||
@@ -489,6 +499,9 @@ export async function searchCustomAvatarItems(
|
|||||||
} else if (search.includeCoachItems === false) {
|
} else if (search.includeCoachItems === false) {
|
||||||
where.push(`creator_account_id != ${bind(COACH_ACCOUNT_ID)}`)
|
where.push(`creator_account_id != ${bind(COACH_ACCOUNT_ID)}`)
|
||||||
}
|
}
|
||||||
|
if (search.creatorAccountId !== undefined) {
|
||||||
|
where.push(`creator_account_id = ${bind(search.creatorAccountId)}`)
|
||||||
|
}
|
||||||
if (search.minPrice !== undefined) where.push(`price >= ${bind(search.minPrice)}`)
|
if (search.minPrice !== undefined) where.push(`price >= ${bind(search.minPrice)}`)
|
||||||
if (search.maxPrice !== undefined) where.push(`price <= ${bind(search.maxPrice)}`)
|
if (search.maxPrice !== undefined) where.push(`price <= ${bind(search.maxPrice)}`)
|
||||||
|
|
||||||
|
|||||||
@@ -710,6 +710,8 @@ export const avatarRoutes = new Hono<App>({ strict: false })
|
|||||||
'stock content (account 1), `false` only player-made items, and leaving it out serves',
|
'stock content (account 1), `false` only player-made items, and leaving it out serves',
|
||||||
'both. The client’s storefront tab sends `True` and its user-generated-content tab',
|
'both. The client’s storefront tab sends `True` and its user-generated-content tab',
|
||||||
'`False`, so the two never overlap.',
|
'`False`, so the two never overlap.',
|
||||||
|
'`creatorAccountId` narrows to one author’s items — what a player’s creations list',
|
||||||
|
'sends (`creatorAccountId=<id>&includeCoachItems=False`); still published items only.',
|
||||||
'`outfitTypes` values are `RecRoom.Avatars.OutfitType` ordinals; the user-generated-',
|
'`outfitTypes` values are `RecRoom.Avatars.OutfitType` ordinals; the user-generated-',
|
||||||
'content tab sends 105 (`CustomShirt`) alone, the storefront tab a dozen slots.',
|
'content tab sends 105 (`CustomShirt`) alone, the storefront tab a dozen slots.',
|
||||||
'`itemTypes` (a separate enum: All -1, None 0, Shirt 1 — not the outfit type),',
|
'`itemTypes` (a separate enum: All -1, None 0, Shirt 1 — not the outfit type),',
|
||||||
@@ -773,6 +775,13 @@ export const avatarRoutes = new Hono<App>({ strict: false })
|
|||||||
'true: only the Coach’s stock items; false: only player-made items; absent: both',
|
'true: only the Coach’s stock items; false: only player-made items; absent: both',
|
||||||
schema: { type: 'boolean' },
|
schema: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'creatorAccountId',
|
||||||
|
in: 'query',
|
||||||
|
required: false,
|
||||||
|
description: 'Only items this account made',
|
||||||
|
schema: { type: 'integer' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'unityAssetTarget',
|
name: 'unityAssetTarget',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
@@ -811,6 +820,7 @@ export const avatarRoutes = new Hono<App>({ strict: false })
|
|||||||
searchQuery: c.req.query('searchQuery'),
|
searchQuery: c.req.query('searchQuery'),
|
||||||
outfitTypes,
|
outfitTypes,
|
||||||
includeCoachItems,
|
includeCoachItems,
|
||||||
|
creatorAccountId: int('creatorAccountId'),
|
||||||
minPrice: int('minPrice'),
|
minPrice: int('minPrice'),
|
||||||
maxPrice: int('maxPrice'),
|
maxPrice: int('maxPrice'),
|
||||||
skip: int('skip'),
|
skip: int('skip'),
|
||||||
|
|||||||
@@ -1238,6 +1238,41 @@ describe('public endpoints', () => {
|
|||||||
all.map((i) => i.Name)
|
all.map((i) => i.Name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// `creatorAccountId` is one author's published items — a player's creations list. It
|
||||||
|
// was once ignored, and every player's shelf showed the whole player-made catalog.
|
||||||
|
const spec2: Array<[name: string, creator: number]> = [['Hat C', 206]]
|
||||||
|
for (const [name, creator] of spec2) {
|
||||||
|
await createCustomAvatarItem(
|
||||||
|
env.DB,
|
||||||
|
{
|
||||||
|
customAvatarItemId: crypto.randomUUID(),
|
||||||
|
creatorAccountId: creator,
|
||||||
|
name,
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
baseAvatarItemId: 1,
|
||||||
|
baseAvatarItemColor: '#fff',
|
||||||
|
accessibility: 1,
|
||||||
|
designFilename: 'design_x.bin',
|
||||||
|
thumbnailImageFilename: 'thumb_x.png',
|
||||||
|
},
|
||||||
|
new Date(Date.UTC(2026, 7, 20))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
expect((await search('?creatorAccountId=206')).map((i) => i.Name)).toEqual(['Hat C'])
|
||||||
|
// The query the client sends for a player's creations: theirs alone, `Hidden` still out.
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await search(
|
||||||
|
'?creatorAccountId=205&includePurchaseInfos=True&includeCoachItems=False&skip=0' +
|
||||||
|
'&take=1000&unityAssetTarget=0&unityAssetVersion=3'
|
||||||
|
)
|
||||||
|
).map((i) => i.Name)
|
||||||
|
).toEqual(['Trousers A', 'Shirt B', 'Shirt A', 'Hat A'])
|
||||||
|
expect(await search('?creatorAccountId=1&includeCoachItems=False')).toEqual([])
|
||||||
|
expect(await search('?creatorAccountId=424242')).toEqual([])
|
||||||
|
await env.DB.prepare('DELETE FROM custom_avatar_item WHERE creator_account_id = 206').run()
|
||||||
|
|
||||||
// The whole storefront query the client actually sends, unchanged — the parameters that
|
// The whole storefront query the client actually sends, unchanged — the parameters that
|
||||||
// aren't acted on yet must be accepted rather than 400 or throw, and `includeCoachItems=True`
|
// aren't acted on yet must be accepted rather than 400 or throw, and `includeCoachItems=True`
|
||||||
// narrows it to the stock content.
|
// narrows it to the stock content.
|
||||||
|
|||||||
Reference in New Issue
Block a user