import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'; import type { Bindings, Variables } from '../types'; import { createTriplet, queryTriplets } from '../actions/triplet-crud'; import { getTripletStats } from '../actions/triplet-stats'; import { ingestEnvelope, IngestEnvelopeSchema } from '../actions/triplet-ingest'; import { makeKbdbClient } from '../lib/kbdb-client'; const tripletRoutes = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>(); const TripletSchema = z.object({ id: z.string(), subject: z.string(), predicate: z.string(), object: z.string(), owner_id: z.string().optional(), }); // GET /triplets/stats (統計) const statsRoute = createRoute({ method: 'get', path: '/stats', responses: { 200: { description: 'Statistics of the knowledge graph' }, }, tags: ['Triplets'], }); tripletRoutes.openapi(statsRoute, async (c) => { const stats = await getTripletStats(makeKbdbClient(c.env)); return c.json(stats, 200); }); // GET /triplets (查詢) const queryRoute = createRoute({ method: 'get', path: '/', request: { query: z.object({ subject: z.string().optional(), predicate: z.string().optional(), object: z.string().optional(), limit: z.string().optional().describe('Maximum number of results (default 50)'), offset: z.string().optional().describe('Pagination offset'), }), }, responses: { 200: { content: { 'application/json': { schema: z.object({ triplets: z.array(TripletSchema), count: z.number() }), }, }, description: 'Query results from KBDB Graph', }, }, tags: ['Triplets'], }); tripletRoutes.openapi(queryRoute, async (c) => { const query = c.req.valid('query'); const limit = query.limit ? parseInt(query.limit, 10) : undefined; const offset = query.offset ? parseInt(query.offset, 10) : undefined; const results = await queryTriplets(makeKbdbClient(c.env), { ...query, limit, offset }); return c.json(results as any, 200); }); // POST /triplets (建立) const createRouteDefinition = createRoute({ method: 'post', path: '/', request: { body: { content: { 'application/json': { schema: z.object({ subject: z.string().min(1), predicate: z.string().min(1), object: z.string().min(1), owner_id: z.string().optional(), source_block_id: z.string().optional(), confidence: z.number().optional(), clusters: z.array(z.string()).optional(), }), }, }, }, }, responses: { 201: { description: 'Triplet created successfully' }, }, tags: ['Triplets'], }); tripletRoutes.openapi(createRouteDefinition, async (c) => { const data = c.req.valid('json'); await createTriplet(makeKbdbClient(c.env), data); return c.json({ ok: true }, 201); }); // POST /triplets/ingest — 收 ingest-candidate envelope(idempotency + deprecate-then-append) const ingestRoute = createRoute({ method: 'post', path: '/ingest', request: { body: { content: { 'application/json': { schema: IngestEnvelopeSchema } } }, }, responses: { 200: { description: 'Envelope ingested (or skipped if same content_hash)' }, 422: { description: 'Invalid envelope (forbidden field or shape mismatch)' }, }, tags: ['Triplets'], }); tripletRoutes.openapi( ingestRoute, async (c) => { const env = c.req.valid('json'); const result = await ingestEnvelope(makeKbdbClient(c.env), env); return c.json(result, 200); }, // strict() 驗證失敗(如送禁止欄位 bridge_score)→ 422,不是預設 400。 (zres, c) => { if (!zres.success) return c.json({ error: 'invalid envelope', issues: zres.error.issues }, 422); }, ); export { tripletRoutes };