[api] fix various issues with inventions

This commit is contained in:
Devin Zuczek
2026-10-02 13:43:48 -04:00
parent 6167925ee0
commit a2f4e4959e
4 changed files with 357 additions and 69 deletions
+79 -22
View File
@@ -34,6 +34,7 @@ import {
deleteInvention,
getFeaturedInventions,
getInventionById,
getInventionsByCreators,
getInventionsByIds,
getInventionsByRoom,
getInventionTagFilters,
@@ -56,6 +57,7 @@ import {
setInventionTags,
toSaveResult,
toSaveResultV9,
unpublishInvention,
updateInvention,
} from '../inventions-db'
import {
@@ -83,6 +85,7 @@ import {
InventionReportRequest,
InventionSaveResult,
InventionSaveV9Result,
InventionV9Dto,
InventionVersionDto,
json,
JsonArray,
@@ -106,6 +109,7 @@ import {
SuccessValueEnvelope,
TagFilters,
UNAUTHORIZED_RESPONSE,
UnpublishInventionRequest,
UpdateCustomAvatarItemRequest,
UpdateInventionMetadataRequest,
UpdateInventionRequest,
@@ -1702,30 +1706,29 @@ export const avatarRoutes = new Hono<App>({ strict: false })
)
// Inventions by particular creators (`?id=207&id=…`) — what the client fills a creator's
// shelf, and the "from creators you follow" row, from.
// portfolio, and the "from creators you follow" row, from.
//
// STUB: an empty list for now. It is the honest answer rather than a placeholder, since
// the client reads it as "this creator has published nothing" and renders an empty
// shelf, where a 404 would read as a row that failed to load. When it becomes real it is
// a filter on the invention table's creator column, the same feed shape as `toptoday`
// and `featured` above — `id` is repeatable, and `skip`/`take` page it.
// Published only, for every caller — a creator's own drafts are `v2/mine`'s alone.
.get(
'/api/inventions/v1/fromcreators',
describeRoute({
tags: ['Inventions'],
summary: 'Inventions by particular creators (stub)',
summary: 'Inventions by particular creators',
description:
'The published inventions of the accounts named by `id` (repeatable), newest first — ' +
'a creator’s shelf, and the "from creators you follow" row. STUB: always an empty ' +
'array for now, which the client renders as "nothing published" rather than as a ' +
'failed load. `id`, `skip` and `take` are accepted and, for the moment, ignored.',
parameters: [
intQuery('id', 'Creator account id; repeatable. Accepted and ignored by the stub'),
...pageParams(100),
],
responses: { 200: json(InventionDto.array(), 'Empty — nothing is served here yet') },
'The inventions of the accounts named by `id` (repeatable), newest first — a ' +
'creator’s portfolio, and the "from creators you follow" row. Only PUBLISHED ' +
'inventions are listed, the caller’s own included: unpublished ones are served by ' +
'`v2/mine` alone. No `id` is an empty array.',
parameters: [intQuery('id', 'Creator account id; repeatable'), ...pageParams(100)],
responses: {
200: json(InventionDto.extend(InventionV9Dto.shape).array(), 'The creators’ inventions'),
},
}),
(c) => c.json([])
async (c) => {
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
const take = Number.parseInt(c.req.query('take') ?? '100', 10) || 100
return c.json(await getInventionsByCreators(c.env.DB, inventionIdQuery(c), skip, take))
}
)
// Invention search/browse: published inventions matching `value` (matched against
@@ -1775,7 +1778,10 @@ export const avatarRoutes = new Hono<App>({ strict: false })
'invention listed even if it has since been unpublished or hidden. Not paginated.',
security: AUTHED,
responses: {
200: json(InventionDto.array(), 'The caller’s inventions'),
200: json(
InventionDto.extend(InventionV9Dto.shape).array(),
'The caller’s inventions — the stored record with the newer client’s flat DTO laid over it'
),
401: UNAUTHORIZED_RESPONSE,
},
}),
@@ -2269,14 +2275,15 @@ export const avatarRoutes = new Hono<App>({ strict: false })
description:
'What puts an invention into search and the feeds. Creator only.\n\n' +
'`Permission` is the `GeneralPermission` other players get, as a raw ladder ' +
'number (the publish sheet sends 20, UseOnly). `Accessibility` says where it can ' +
'be found — 1 (Public) lists it, 2 (Unlisted) publishes it reachable by id but ' +
'keeps it out of browse and search. A null `Price` leaves the price alone rather ' +
'number (the publish sheet sends 20, UseOnly). `Accessibility` says whether it ' +
'is in the store — 1 (Public) lists it in search and the feeds, 2 (Unlisted) ' +
'publishes it onto the creator’s portfolio and reachable by id but keeps it out ' +
'of both. A null `Price` leaves the price alone rather ' +
'than zeroing it, so re-publishing something that was for sale doesn’t give it ' +
'away; every field but `InventionId` is nullable and an omitted one keeps what ' +
'the invention has. A price over 1000 tokens (`MAX_INVENTION_PRICE`) refuses the ' +
'publish in-band.\n\n' +
'Publishing is not undone here, and re-publishing doesn’t re-date the first ' +
'Publishing is undone by `v2/unpublish`, and re-publishing doesn’t re-date the first ' +
'publish. Refusals answer `Success: false` with a null `Value`, the way ' +
'`v9/save` does.',
security: AUTHED,
@@ -2327,6 +2334,56 @@ export const avatarRoutes = new Hono<App>({ strict: false })
}
)
// Unpublish an invention: `v4/publish` undone. Same body shape as `v2/delete` — the id
// and nothing else — and creator only.
//
// The reply is the `v9/save` envelope carrying the UPDATED invention, as `v4/publish`
// answers. That is an ASSUMPTION: what the client reads off this response has not been
// observed, only the request.
.post(
'/api/inventions/v2/unpublish',
describeRoute({
tags: ['Inventions'],
summary: 'Unpublish an invention',
description:
'Creator only. Takes the invention back out of search, the feeds and the ' +
'creator’s portfolio: `IsPublished` goes false and `Accessibility` back to 0, the ' +
'state a save mints, so the next `v4/publish` sets both afresh. Players who ' +
'already bought it keep it, the price and permission are left as they are, and ' +
'the first-publish date is not cleared.\n\n' +
'Answers the enveloped result `v4/publish` answers, carrying the unpublished ' +
'invention. Refusals are `Success: false` with a null `Value`.',
security: AUTHED,
requestBody: jsonBody(UnpublishInventionRequest, 'The invention to unpublish'),
responses: {
200: json(
InventionSaveV9Result,
'The envelope — the unpublished invention under `Value`, or `Success: false` ' +
'with `Error` when it was refused'
),
401: json(InventionSaveV9Result, 'The same envelope, refused — not an empty body'),
},
}),
async (c) => {
const body = (await c.req.json().catch(() => null)) as Record<string, unknown> | null
if (body === null) return c.json(inventionSaveV9Failure('Invalid request body'))
const gate = await creatorsInventionResult(
c,
typeof body.InventionId === 'number' ? body.InventionId : Number.NaN
)
if ('rejection' in gate) {
return gate.status === 401
? c.json(inventionSaveV9Failure(gate.rejection), 401)
: c.json(inventionSaveV9Failure(gate.rejection))
}
const unpublished = await unpublishInvention(c.env.DB, gate.invention.InventionId)
if (unpublished === null) return c.json(inventionSaveV9Failure('No such invention'))
return c.json(toSaveResultV9(unpublished, unpublished.Tags ?? []))
}
)
// Delete an invention. The newer client's shape: a POST with a PascalCase body
// carrying nothing but the id. Auth-gated, creator only — the only thing that may
// remove an invention is the account that made it, not a co-owner and not a buyer.