mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -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.
|
||||
*/
|
||||
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. */
|
||||
minPrice?: number
|
||||
/** 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
|
||||
* 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
|
||||
* 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
|
||||
@@ -489,6 +499,9 @@ export async function searchCustomAvatarItems(
|
||||
} else if (search.includeCoachItems === false) {
|
||||
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.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',
|
||||
'both. The client’s storefront tab sends `True` and its user-generated-content tab',
|
||||
'`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-',
|
||||
'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),',
|
||||
@@ -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',
|
||||
schema: { type: 'boolean' },
|
||||
},
|
||||
{
|
||||
name: 'creatorAccountId',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Only items this account made',
|
||||
schema: { type: 'integer' },
|
||||
},
|
||||
{
|
||||
name: 'unityAssetTarget',
|
||||
in: 'query',
|
||||
@@ -811,6 +820,7 @@ export const avatarRoutes = new Hono<App>({ strict: false })
|
||||
searchQuery: c.req.query('searchQuery'),
|
||||
outfitTypes,
|
||||
includeCoachItems,
|
||||
creatorAccountId: int('creatorAccountId'),
|
||||
minPrice: int('minPrice'),
|
||||
maxPrice: int('maxPrice'),
|
||||
skip: int('skip'),
|
||||
|
||||
@@ -1238,6 +1238,41 @@ describe('public endpoints', () => {
|
||||
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
|
||||
// aren't acted on yet must be accepted rather than 400 or throw, and `includeCoachItems=True`
|
||||
// narrows it to the stock content.
|
||||
|
||||
Reference in New Issue
Block a user