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

+293
View File
@@ -0,0 +1,293 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import type { AppEnv } from "../config/env.js";
import {
findFeature,
listFeatureGroupsForProject as listFixtureFeatureGroupsForProject,
listFeaturesForProject as listFixtureFeaturesForProject,
paginate,
type PaginationInput,
} from "../fixtures/featureFixtures.js";
import { findProject } from "../fixtures/projectFixtures.js";
import { getRequestId } from "../plugins/requestId.js";
import {
listFeatureGroupsForProject as listDatabaseFeatureGroupsForProject,
} from "../repositories/featureGroupsRepository.js";
import {
getFeatureBySlugOrId,
listFeaturesForProject as listDatabaseFeaturesForProject,
projectExists as databaseProjectExists,
} from "../repositories/featuresRepository.js";
import type { FeatureDto, FeatureStatus } from "../types/api.js";
import { errorEnvelope, successEnvelope } from "../utils/envelope.js";
const idSchema = z.string().min(1).regex(/^[a-z0-9-]+$/);
const projectParamsSchema = z.object({
projectId: idSchema,
});
const featureParamsSchema = z.object({
projectId: idSchema,
featureId: idSchema,
});
const booleanQuerySchema = z.preprocess(
(value) => {
if (value === undefined || typeof value === "boolean") return value;
if (typeof value !== "string") return undefined;
const normalized = value.toLowerCase();
if (["true", "1", "yes", "on"].includes(normalized)) return true;
if (["false", "0", "no", "off"].includes(normalized)) return false;
return undefined;
},
z.boolean().optional(),
);
const paginationQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
pageSize: z.coerce.number().int().positive().max(100).default(25),
cursor: z.string().optional(),
sort: z.string().optional(),
});
const featuresQuerySchema = paginationQuerySchema.extend({
status: z.string().optional(),
confidence: z.coerce.number().int().min(0).max(100).optional(),
hasEvidence: booleanQuerySchema,
groupId: idSchema.optional(),
search: z.string().optional(),
});
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 parseQuery = <TSchema extends z.ZodTypeAny>(
schema: TSchema,
request: FastifyRequest,
reply: FastifyReply,
): z.output<TSchema> | undefined => {
const parsed = schema.safeParse(request.query);
if (!parsed.success) {
sendValidationError(request, reply, parsed.error.issues);
return undefined;
}
return parsed.data;
};
const sendProjectNotFound = (
request: FastifyRequest,
reply: FastifyReply,
projectId: string,
) =>
reply.status(404).send(
errorEnvelope(
"PROJECT_NOT_FOUND",
`Project '${projectId}' was not found.`,
getRequestId(request),
),
);
const splitCsv = (value: string | undefined): string[] =>
value
?.split(",")
.map((item) => item.trim())
.filter(Boolean) ?? [];
const sortByField = <T extends Record<string, unknown>>(
items: T[],
sort: string | undefined,
): T[] => {
if (!sort) return items;
const [field, direction = "asc"] = sort.split(":");
if (!field) return items;
return [...items].sort((left, right) => {
const leftValue = left[field];
const rightValue = right[field];
if (leftValue === rightValue) return 0;
const comparison = String(leftValue ?? "").localeCompare(
String(rightValue ?? ""),
undefined,
{ numeric: true },
);
return direction === "desc" ? -comparison : comparison;
});
};
type FeatureQuery = z.output<typeof featuresQuerySchema>;
type PaginationQuery = z.output<typeof paginationQuerySchema>;
const applyFeatureFilters = (
features: FeatureDto[],
query: FeatureQuery,
): FeatureDto[] => {
const statuses = splitCsv(query.status) as FeatureStatus[];
const normalizedSearch = query.search?.trim().toLowerCase();
return features.filter((feature) => {
if (statuses.length > 0 && !statuses.includes(feature.status)) return false;
if (query.confidence !== undefined && feature.confidence !== query.confidence) {
return false;
}
if (query.groupId !== undefined && feature.groupId !== query.groupId) {
return false;
}
if (
query.hasEvidence !== undefined &&
(feature.evidenceIds.length > 0) !== query.hasEvidence
) {
return false;
}
if (
normalizedSearch &&
!`${feature.name} ${feature.description}`
.toLowerCase()
.includes(normalizedSearch)
) {
return false;
}
return true;
});
};
const toPaginationInput = (
query: PaginationQuery,
filters: PaginationInput["filters"],
): PaginationInput => ({
page: query.page,
pageSize: query.pageSize,
cursor: query.cursor,
sort: query.sort,
filters,
});
const ensureProject = async (
request: FastifyRequest,
reply: FastifyReply,
projectId: string,
useDatabase: boolean,
) => {
const exists = useDatabase
? await databaseProjectExists(projectId)
: Boolean(findProject(projectId));
if (!exists) {
sendProjectNotFound(request, reply, projectId);
return false;
}
return true;
};
type FeatureRoutesOptions = {
dataSource?: AppEnv["PROJECTBRAIN_DATA_SOURCE"];
};
export const registerFeatureRoutes = async (
app: FastifyInstance,
options: FeatureRoutesOptions = {},
) => {
const useDatabase = options.dataSource === "database";
app.get("/api/projects/:projectId/feature-groups", async (request, reply) => {
const params = parseParams(projectParamsSchema, request, reply);
if (!params) return reply;
if (!(await ensureProject(request, reply, params.projectId, useDatabase))) {
return reply;
}
const groups = useDatabase
? await listDatabaseFeatureGroupsForProject(params.projectId)
: listFixtureFeatureGroupsForProject(params.projectId);
return successEnvelope(groups ?? [], getRequestId(request));
});
app.get("/api/projects/:projectId/features", async (request, reply) => {
const params = parseParams(projectParamsSchema, request, reply);
if (!params) return reply;
if (!(await ensureProject(request, reply, params.projectId, useDatabase))) {
return reply;
}
const query = parseQuery(featuresQuerySchema, request, reply);
if (!query) return reply;
const filters = {
...(query.status ? { status: splitCsv(query.status) } : {}),
...(query.confidence !== undefined ? { confidence: query.confidence } : {}),
...(query.hasEvidence !== undefined ? { hasEvidence: query.hasEvidence } : {}),
...(query.groupId ? { groupId: query.groupId } : {}),
...(query.search ? { search: query.search } : {}),
...(query.cursor ? { cursor: query.cursor } : {}),
};
const sourceFeatures = useDatabase
? await listDatabaseFeaturesForProject(params.projectId)
: listFixtureFeaturesForProject(params.projectId);
const filteredFeatures = sortByField(
applyFeatureFilters(sourceFeatures ?? [], query),
query.sort,
);
return successEnvelope(
paginate(filteredFeatures, toPaginationInput(query, filters)),
getRequestId(request),
);
});
app.get("/api/projects/:projectId/features/:featureId", async (request, reply) => {
const params = parseParams(featureParamsSchema, request, reply);
if (!params) return reply;
if (!(await ensureProject(request, reply, params.projectId, useDatabase))) {
return reply;
}
const feature = useDatabase
? await getFeatureBySlugOrId(params.projectId, params.featureId)
: findFeature(params.projectId, params.featureId);
if (!feature) {
return reply.status(404).send(
errorEnvelope(
"FEATURE_NOT_FOUND",
`Feature '${params.featureId}' was not found.`,
getRequestId(request),
),
);
}
return successEnvelope(feature, getRequestId(request));
});
};