Fas 1a punkt 2: release_gates tabell + go/no-go-vy
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { schema, evaluateReleaseGates, seedReleaseGates, summarizeReleaseGates } from "@app/database";
|
||||
import { releaseGateUpdateSchema } from "@app/validation";
|
||||
import { z } from "zod";
|
||||
import { errors, parse } from "../lib/errors.js";
|
||||
import { audit } from "../lib/helpers.js";
|
||||
|
||||
/**
|
||||
* Release gate administration (spec §17).
|
||||
* Lists built-in go/no-go criteria, evaluates them against analytics events,
|
||||
* and lets admins tune thresholds.
|
||||
*/
|
||||
export async function adminReleaseGateRoutes(app: FastifyInstance) {
|
||||
const admin = { preHandler: [app.requireAdmin] };
|
||||
|
||||
/** Seed built-in gates. Safe to call multiple times. */
|
||||
app.post("/admin/v1/release-gates/seed", admin, async (req) => {
|
||||
await seedReleaseGates(app.db);
|
||||
await audit(app.db, {
|
||||
actorUserId: req.userId,
|
||||
actorType: "admin",
|
||||
action: "release_gates.seeded",
|
||||
});
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
/** Evaluate all gates now. */
|
||||
app.post("/admin/v1/release-gates/evaluate", admin, async (req) => {
|
||||
const gates = await evaluateReleaseGates(app.db);
|
||||
const summary = summarizeReleaseGates(gates);
|
||||
await audit(app.db, {
|
||||
actorUserId: req.userId,
|
||||
actorType: "admin",
|
||||
action: "release_gates.evaluated",
|
||||
metadata: { summary },
|
||||
});
|
||||
return { gates, summary };
|
||||
});
|
||||
|
||||
/** List current gate definitions without re-evaluating. */
|
||||
app.get("/admin/v1/release-gates", admin, async () => {
|
||||
const gates = await app.db.select().from(schema.releaseGates).orderBy(schema.releaseGates.category, schema.releaseGates.gateKey);
|
||||
const summary = summarizeReleaseGates(
|
||||
gates.map((g) => ({
|
||||
gateKey: g.gateKey,
|
||||
category: g.category,
|
||||
nameSv: g.nameSv,
|
||||
nameEn: g.nameEn,
|
||||
targetValue: g.targetValue,
|
||||
comparison: g.comparison,
|
||||
value: g.lastValue,
|
||||
status: g.status,
|
||||
lastEvaluatedAt: g.lastEvaluatedAt?.toISOString() ?? new Date().toISOString(),
|
||||
blocking: g.blocking,
|
||||
notes: g.notes ?? undefined,
|
||||
})),
|
||||
);
|
||||
return { gates, summary };
|
||||
});
|
||||
|
||||
/** Update gate configuration. */
|
||||
app.put("/admin/v1/release-gates/:key", admin, async (req) => {
|
||||
const params = z.object({ key: z.string().max(64) }).parse(req.params);
|
||||
const input = parse(releaseGateUpdateSchema, req.body);
|
||||
|
||||
const [existing] = await app.db
|
||||
.select()
|
||||
.from(schema.releaseGates)
|
||||
.where(eq(schema.releaseGates.gateKey, params.key))
|
||||
.limit(1);
|
||||
if (!existing) throw errors.notFound("Gate finns inte.");
|
||||
|
||||
const [updated] = await app.db
|
||||
.update(schema.releaseGates)
|
||||
.set({
|
||||
...input,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.releaseGates.gateKey, params.key))
|
||||
.returning();
|
||||
|
||||
await audit(app.db, {
|
||||
actorUserId: req.userId,
|
||||
actorType: "admin",
|
||||
action: "release_gates.updated",
|
||||
targetType: "release_gate",
|
||||
targetId: params.key,
|
||||
metadata: input,
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user