Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5
This commit is contained in:
commit
5aa3e8d685
700 files changed
+132801
No files matched your search
@@ -0,0 +1,86 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
import type { AppEnv } from "../config/env.js";
|
||||
import { convertBrainstormForProject, createBrainstormForProject, listBrainstormsForProject, updateBrainstormForProject } from "../repositories/brainstormRepository.js";
|
||||
import { getRequestId } from "../plugins/requestId.js";
|
||||
import { errorEnvelope, successEnvelope } from "../utils/envelope.js";
|
||||
import { getIdempotencyKey, getIdempotentResponse, setIdempotentResponse } from "../utils/idempotency.js";
|
||||
|
||||
type BrainstormRouteOptions = { dataSource?: AppEnv["PROJECTBRAIN_DATA_SOURCE"] };
|
||||
const idSchema = z.string().min(1).regex(/^[A-Za-z0-9-]+$/);
|
||||
const projectParamsSchema = z.object({ projectId: idSchema });
|
||||
const brainstormParamsSchema = z.object({ projectId: idSchema, brainstormId: idSchema });
|
||||
const bodySchema = z.object({
|
||||
title: z.string().min(2).max(200),
|
||||
prompt: z.string().min(1).max(8000),
|
||||
scope: z.string().min(1).max(80),
|
||||
mode: z.string().min(1).max(80),
|
||||
status: z.string().max(80).optional(),
|
||||
targetEntityId: z.string().max(200).optional(),
|
||||
targetEntityType: z.string().max(80).optional(),
|
||||
generatedBy: z.string().max(80).optional(),
|
||||
contextSummary: z.string().max(1000).optional(),
|
||||
ideas: z.array(z.unknown()).optional(),
|
||||
clusters: z.array(z.unknown()).optional(),
|
||||
outcomes: z.array(z.unknown()).optional(),
|
||||
selectedIdeaIds: z.array(z.string()).optional(),
|
||||
nextActions: z.array(z.string()).optional(),
|
||||
});
|
||||
const patchSchema = bodySchema.partial();
|
||||
const conversionTargets = ["feature", "documentation", "context_pack", "blueprint_node", "initiative"] as const;
|
||||
const convertParamsSchema = z.object({ projectId: idSchema, brainstormId: idSchema });
|
||||
const convertBodySchema = z.object({ target: z.enum(conversionTargets) });
|
||||
|
||||
const sendValidationError = (request: FastifyRequest, reply: FastifyReply, validation: z.ZodIssue[]) => reply.status(400).send(errorEnvelope("VALIDATION_ERROR", "Request validation failed", getRequestId(request), validation.map((issue) => ({ field: issue.path.join("."), message: issue.message, code: issue.code }))));
|
||||
const parseParams = <TSchema extends z.ZodTypeAny>(schema: TSchema, request: FastifyRequest, reply: FastifyReply): z.output<TSchema> | undefined => { const parsed = schema.safeParse(request.params); if (!parsed.success) { sendValidationError(request, reply, parsed.error.issues); return undefined; } return parsed.data; };
|
||||
const getRequiredIdempotencyKey = (request: FastifyRequest, reply: FastifyReply): string | undefined => { const rawKey = request.headers["idempotency-key"]; const idempotencyKey = Array.isArray(rawKey) ? rawKey[0] : rawKey; if (!idempotencyKey?.trim()) { reply.status(400).send(errorEnvelope("IDEMPOTENCY_KEY_REQUIRED", "Idempotency-Key header is required.", getRequestId(request))); return undefined; } return idempotencyKey; };
|
||||
|
||||
export const registerBrainstormRoutes = async (app: FastifyInstance, options: BrainstormRouteOptions = {}) => {
|
||||
const dataSource = options.dataSource ?? "fixture";
|
||||
|
||||
app.get("/api/projects/:projectId/brainstorms", async (request, reply) => {
|
||||
const params = parseParams(projectParamsSchema, request, reply); if (!params) return reply;
|
||||
if (dataSource !== "database") return reply.status(501).send(errorEnvelope("DATABASE_MODE_REQUIRED", "Brainstorm persistence is only available when PROJECTBRAIN_DATA_SOURCE=database.", getRequestId(request)));
|
||||
return successEnvelope({ items: await listBrainstormsForProject(params.projectId) }, getRequestId(request));
|
||||
});
|
||||
|
||||
app.post("/api/projects/:projectId/brainstorms", async (request, reply) => {
|
||||
const params = parseParams(projectParamsSchema, request, reply); if (!params) return reply;
|
||||
if (dataSource !== "database") return reply.status(501).send(errorEnvelope("DATABASE_MODE_REQUIRED", "Brainstorm persistence is only available when PROJECTBRAIN_DATA_SOURCE=database.", getRequestId(request)));
|
||||
const idempotencyKey = getRequiredIdempotencyKey(request, reply); if (!idempotencyKey) return reply;
|
||||
const cacheKey = getIdempotencyKey(request.method, request.url, idempotencyKey);
|
||||
const replay = getIdempotentResponse<unknown>(cacheKey); if (replay) return reply.status(replay.statusCode).send(replay.body);
|
||||
const parsed = bodySchema.safeParse(request.body); if (!parsed.success) return sendValidationError(request, reply, parsed.error.issues);
|
||||
const body = successEnvelope(await createBrainstormForProject(params.projectId, parsed.data), getRequestId(request));
|
||||
setIdempotentResponse(cacheKey, { statusCode: 201, body });
|
||||
return reply.status(201).send(body);
|
||||
});
|
||||
|
||||
app.patch("/api/projects/:projectId/brainstorms/:brainstormId", async (request, reply) => {
|
||||
const params = parseParams(brainstormParamsSchema, request, reply); if (!params) return reply;
|
||||
if (dataSource !== "database") return reply.status(501).send(errorEnvelope("DATABASE_MODE_REQUIRED", "Brainstorm persistence is only available when PROJECTBRAIN_DATA_SOURCE=database.", getRequestId(request)));
|
||||
const idempotencyKey = getRequiredIdempotencyKey(request, reply); if (!idempotencyKey) return reply;
|
||||
const cacheKey = getIdempotencyKey(request.method, request.url, idempotencyKey);
|
||||
const replay = getIdempotentResponse<unknown>(cacheKey); if (replay) return reply.status(replay.statusCode).send(replay.body);
|
||||
const parsed = patchSchema.safeParse(request.body); if (!parsed.success) return sendValidationError(request, reply, parsed.error.issues);
|
||||
const item = await updateBrainstormForProject(params.projectId, params.brainstormId, parsed.data);
|
||||
if (!item) return reply.status(404).send(errorEnvelope("BRAINSTORM_NOT_FOUND", `Brainstorm '${params.brainstormId}' was not found.`, getRequestId(request)));
|
||||
const body = successEnvelope(item, getRequestId(request));
|
||||
setIdempotentResponse(cacheKey, { statusCode: 200, body });
|
||||
return reply.status(200).send(body);
|
||||
});
|
||||
|
||||
app.post("/api/projects/:projectId/brainstorms/:brainstormId/convert", async (request, reply) => {
|
||||
const params = parseParams(convertParamsSchema, request, reply); if (!params) return reply;
|
||||
if (dataSource !== "database") return reply.status(501).send(errorEnvelope("DATABASE_MODE_REQUIRED", "Brainstorm conversion persistence is only available when PROJECTBRAIN_DATA_SOURCE=database.", getRequestId(request)));
|
||||
const idempotencyKey = getRequiredIdempotencyKey(request, reply); if (!idempotencyKey) return reply;
|
||||
const cacheKey = getIdempotencyKey(request.method, request.url, idempotencyKey);
|
||||
const replay = getIdempotentResponse<unknown>(cacheKey); if (replay) return reply.status(replay.statusCode).send(replay.body);
|
||||
const parsed = convertBodySchema.safeParse(request.body); if (!parsed.success) return sendValidationError(request, reply, parsed.error.issues);
|
||||
const result = await convertBrainstormForProject(params.projectId, params.brainstormId, parsed.data.target);
|
||||
if (!result) return reply.status(404).send(errorEnvelope("BRAINSTORM_NOT_FOUND", `Brainstorm '${params.brainstormId}' was not found.`, getRequestId(request)));
|
||||
const body = successEnvelope(result, getRequestId(request));
|
||||
setIdempotentResponse(cacheKey, { statusCode: 200, body });
|
||||
return reply.status(200).send(body);
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user