// POST /v1/products — create a product. GUARDED + IDEMPOTENT. // // - `guards: [requireScope('products:write')]` → 403 unless the resolved // Subject carries that scope. The app's `apiKeyStrategy` (see app.config) // maps the demo bearer token to a subject WITH the scope; an anonymous // caller is rejected. // - `idempotency: true` in app.config makes a POST carrying an // `Idempotency-Key` header replay-safe: a retry with the same key replays // the first response instead of inserting twice. // // The `body` is parsed from the JSON request body and decoded against the // `body` sub-schema (a bad shape → 400 before the handler runs). import { defineRestRoute, requireScope } from '@voltro/protocol/rest' import { Schema } from 'effect' import { randomUUID } from 'node:crypto' import type { DataStore } from '@voltro/runtime' import { Product, toProduct } from '../../lib/product' export default defineRestRoute({ method: 'POST', path: '/v1/products', input: Schema.Struct({ body: Schema.Struct({ name: Schema.NonEmptyString, priceCents: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0)), }), }), output: Product, summary: 'Create a product', guards: [requireScope('products:write')], handler: async ({ body }, ctx) => { const store = ctx.store as DataStore // Explicit id + createdAt — the REST store path doesn't run the mutation // middleware that auto-injects them, so we own them here. const row = { id: `prod_${randomUUID().replace(/-/g, '')}`, name: body.name, priceCents: body.priceCents, createdAt: new Date(), } await store.insert('products', row as never) return toProduct(row) }, })