From 2c515600ef2e435b7278d23ae15c7d9f99f26073 Mon Sep 17 00:00:00 2001 From: Andrea Debernardi Date: Tue, 13 Oct 2026 23:01:15 +0200 Subject: [PATCH] feat(api): backfill request dates from the admin endpoint PATCH /api/admin/requests/:id (admin_update_request over MCP) accepts createdAt and fulfilledAt (epoch ms), so requests imported from a curated list can carry their real history. fulfilledAt only applies to fulfilled requests. --- .../api/src/routes/api/admin/requests/[id].ts | 19 ++++++++++++++-- .../tests/routes/request-lifecycle.test.ts | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/api/src/routes/api/admin/requests/[id].ts b/apps/api/src/routes/api/admin/requests/[id].ts index 78b52f8..12febee 100644 --- a/apps/api/src/routes/api/admin/requests/[id].ts +++ b/apps/api/src/routes/api/admin/requests/[id].ts @@ -39,6 +39,8 @@ export default new Elysia() if (body.description !== undefined) patch.description = body.description if (body.kind !== undefined) patch.kind = body.kind if (body.bounty !== undefined) patch.bounty = body.bounty === null ? null : JSON.stringify(body.bounty) + // Backfilling history (e.g. requests imported from a curated list). + if (body.createdAt !== undefined) patch.createdAt = body.createdAt let event: WebhookEvent | null = null if (body.status !== undefined && body.status !== row.status) { @@ -54,7 +56,7 @@ export default new Elysia() return { error: `No plugin '${pluginId}' to link the request to.` } } patch.fulfilledPluginId = plugin.id - patch.fulfilledAt = now + patch.fulfilledAt = body.fulfilledAt ?? now } else { patch.fulfilledPluginId = null patch.fulfilledAt = null @@ -65,6 +67,10 @@ export default new Elysia() } else if (body.status === 'rejected' && body.rejectionReason !== undefined) { patch.rejectionReason = body.rejectionReason } + // Only a shipped request has a shipping date. + if (body.fulfilledAt !== undefined && (patch.status ?? row.status) === 'fulfilled') { + patch.fulfilledAt = body.fulfilledAt + } await db.update(pluginRequests).set(patch).where(eq(pluginRequests.id, params.id)) const updated = (await db.query.pluginRequests.findFirst({ where: { id: params.id } }))! @@ -89,7 +95,7 @@ export default new Elysia() tags: ['Admin'], summary: 'Review or edit a plugin request', description: - 'Approve, reject (with an optional reason), reopen as pending or mark fulfilled a request, and edit its name, description, kind and bounty board metadata. Status changes emit `request.approved`, `request.rejected` or `request.fulfilled`. `fulfilledPluginId` defaults to the request slug.', + 'Approve, reject (with an optional reason), reopen as pending or mark fulfilled a request, and edit its name, description, kind and bounty board metadata. `createdAt` and `fulfilledAt` (epoch ms) backfill dates for requests imported from elsewhere; `fulfilledAt` only applies to fulfilled requests. Status changes emit `request.approved`, `request.rejected` or `request.fulfilled`. `fulfilledPluginId` defaults to the request slug.', operationId: 'adminUpdateRequest', security: [{ bearerAuth: [] }, { cookieAuth: [] }], }, @@ -104,6 +110,15 @@ export default new Elysia() description: t.Optional(t.String({ minLength: 1, maxLength: 2000 })), kind: t.Optional(t.Nullable(t.String({ maxLength: 40 }))), bounty: t.Optional(t.Nullable(bountySchema)), + createdAt: t.Optional( + t.Integer({ minimum: 0, description: 'When the request was made (epoch ms), to backfill history.' }), + ), + fulfilledAt: t.Optional( + t.Integer({ + minimum: 0, + description: 'When the request shipped (epoch ms). Only applies to fulfilled requests.', + }), + ), }), response: { 200: requestViewSchema, 400: errorSchema, 404: errorSchema }, }, diff --git a/apps/api/tests/routes/request-lifecycle.test.ts b/apps/api/tests/routes/request-lifecycle.test.ts index 6601cd2..b4520a2 100644 --- a/apps/api/tests/routes/request-lifecycle.test.ts +++ b/apps/api/tests/routes/request-lifecycle.test.ts @@ -261,3 +261,25 @@ describe('fulfillment on first manifest', () => { expect((await db.query.pluginRequests.findFirst({ where: { id: row.id } }))?.status).toBe('fulfilled') }) }) + +describe('PATCH /api/admin/requests/:id — backfilled dates', () => { + beforeEach(clearDb) + + it('sets createdAt and, on fulfilled requests only, fulfilledAt', async () => { + const user = await makeUser() + const admin = await makeAdmin() + const open = await seedRequest(user, { slug: 'firebird', status: 'approved' }) + const shipped = await seedRequest(user, { slug: 'oracle', status: 'fulfilled', fulfilledAt: 1 }) + + await call( + `/api/admin/requests/${open.id}`, + json({ createdAt: 1777000000000, fulfilledAt: 1 }, adminHeaders(admin)), + ) + await call(`/api/admin/requests/${shipped.id}`, json({ fulfilledAt: 1790000000000 }, adminHeaders(admin))) + + const a = await db.query.pluginRequests.findFirst({ where: { id: open.id } }) + const b = await db.query.pluginRequests.findFirst({ where: { id: shipped.id } }) + expect(a).toMatchObject({ createdAt: 1777000000000, fulfilledAt: null }) + expect(b?.fulfilledAt).toBe(1790000000000) + }) +})