Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:43:26 +02:00
commit 5aa3e8d685
700 files changed
+132801

No files matched your search

+86
View File
@@ -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);
});
};