import { Router } from 'express'; import { z } from 'zod'; import * as repo from '../../db/repos/source_docs.js'; import { validate } from '../validate.js'; import { NotFoundError, ValidationError, asyncWrap } from '../errors.js'; const baseFields = { name: z.string().min(1).max(200), upstream_url: z.string().min(1).max(2048), version: z.string().nullable().optional(), format: z.string().nullable().optional(), sync_source: z.string().nullable().optional(), local_path: z.string().nullable().optional(), body_text: z.string().nullable().optional(), last_synced: z.string().datetime().nullable().optional(), metadata: z.record(z.string(), z.any()).nullable().optional() }; const createSchema = z.object(baseFields); const patchSchema = z.object(Object.fromEntries( Object.entries(baseFields).map(([k, v]) => [k, v.optional()]) )); const idParams = z.object({ id: z.string().uuid() }); const resourceParams = z.object({ resource_id: z.string().uuid() }); export const router = Router(); export const resourcesScopedRouter = Router({ mergeParams: true }); resourcesScopedRouter.post('/', validate({ params: resourceParams, body: createSchema }), asyncWrap(async (req, res) => { try { const row = await repo.create({ ...req.body, resource_id: req.params.resource_id }, req.actor); res.status(201).json(row); } catch (e) { if (e.code === '23503') throw new ValidationError('invalid resource', { resource_id: req.params.resource_id }); throw e; } }) ); router.get('/:id', validate({ params: idParams }), asyncWrap(async (req, res) => { const row = await repo.getById(req.params.id); if (!row) throw new NotFoundError('source doc not found'); res.json(row); }) ); router.patch('/:id', validate({ params: idParams, body: patchSchema }), asyncWrap(async (req, res) => { const existing = await repo.getById(req.params.id); if (!existing) throw new NotFoundError('source doc not found'); res.json(await repo.update(req.params.id, req.body, req.actor)); }) ); router.delete('/:id', validate({ params: idParams }), asyncWrap(async (req, res) => { const existing = await repo.getById(req.params.id); if (!existing) throw new NotFoundError('source doc not found'); await repo.del(req.params.id, req.actor); res.status(204).end(); }) ); // Resync hooks into the pg-boss worker queue in Plan 3. Gated behind // ENABLE_RESYNC so the route exists for the UI but doesn't enqueue // jobs that have no consumer yet. router.post('/:id/resync', validate({ params: idParams }), asyncWrap(async (req, res) => { const existing = await repo.getById(req.params.id); if (!existing) throw new NotFoundError('source doc not found'); if (process.env.ENABLE_RESYNC !== 'true') { return res.status(503).json({ error: { code: 'feature_disabled', message: 'resync workers land in Plan 3' } }); } res.status(202).json({ queued: true, note: 'workers land in Plan 3' }); }) );