关联 Issue: P25-1, P25-2, P25-3, P25-4, P25-5, P26-1, P26-3, P24-1, P24-2, P29-1, P29-2, P29-3, P29-4
81 行
3.9 KiB
JavaScript
81 行
3.9 KiB
JavaScript
const { z } = require("zod");
|
|
|
|
const { AppError } = require("../utils/AppError");
|
|
const { sendData } = require("../utils/reply");
|
|
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
|
|
|
const ID_SCHEMA = z.string().uuid();
|
|
const SCOPE_TYPE_SCHEMA = z.enum(["department", "legal_entity", "domain"]);
|
|
const WRITE_SCHEMA = z.object({
|
|
scopeType: SCOPE_TYPE_SCHEMA,
|
|
scopeKey: z.string().trim().min(1).max(64),
|
|
enabled: z.boolean().optional(),
|
|
startsAt: z.string().datetime({ offset: true }).nullable().optional(),
|
|
notes: z.string().trim().max(500).nullable().optional(),
|
|
}).strict();
|
|
const PATCH_SCHEMA = WRITE_SCHEMA.partial().refine((value) => Object.keys(value).length > 0, { message: "empty_patch" });
|
|
|
|
function buildValidationError(details, statusCode = 400) { return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details }); }
|
|
function parseId(value) { const parsed = ID_SCHEMA.safeParse(value); if (!parsed.success) throw buildValidationError({ id: "invalid_uuid" }); return parsed.data; }
|
|
function parseBody(schema, body) { const parsed = schema.safeParse(body ?? {}); if (!parsed.success) throw buildValidationError(parsed.error.flatten()); return parsed.data; }
|
|
async function ensureFeatureEnabled(fastify) {
|
|
const gate = await evaluateFeatureFlag({ prisma: fastify.prisma, config: fastify.config, flagKey: FEATURE_FLAG_KEYS.cutoverConsole, strictDepartment: false });
|
|
if (!gate.enabled) throw buildValidationError({ featureFlag: "cutover_console_v1_disabled", reason: gate.reason }, 409);
|
|
}
|
|
function mapScope(item) {
|
|
return {
|
|
id: item.id,
|
|
scopeType: item.scope_type,
|
|
scopeKey: item.scope_key,
|
|
enabled: item.enabled,
|
|
startsAt: item.starts_at?.toISOString?.() ?? null,
|
|
notes: item.notes ?? null,
|
|
createdAt: item.created_at?.toISOString?.() ?? null,
|
|
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
|
updatedBy: item.updated_by ?? null,
|
|
};
|
|
}
|
|
async function cutoverRoutes(fastify) {
|
|
fastify.get("/cutover/scopes", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
|
|
await ensureFeatureEnabled(fastify);
|
|
const items = await fastify.prisma.cutoverScope.findMany({ orderBy: [{ scope_type: "asc" }, { scope_key: "asc" }] });
|
|
return sendData(reply, request, { items: items.map(mapScope) });
|
|
});
|
|
|
|
fastify.post("/cutover/scopes", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
|
|
await ensureFeatureEnabled(fastify);
|
|
const body = parseBody(WRITE_SCHEMA, request.body);
|
|
const created = await fastify.prisma.cutoverScope.create({
|
|
data: {
|
|
scope_type: body.scopeType,
|
|
scope_key: body.scopeKey,
|
|
enabled: body.enabled ?? false,
|
|
starts_at: body.startsAt ? new Date(body.startsAt) : null,
|
|
notes: body.notes ?? null,
|
|
created_by: request.employee.id,
|
|
updated_by: request.employee.id,
|
|
},
|
|
});
|
|
return reply.code(201).send({ requestId: request.id, data: { item: mapScope(created) } });
|
|
});
|
|
|
|
fastify.patch("/cutover/scopes/:id", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
|
|
await ensureFeatureEnabled(fastify);
|
|
const scopeId = parseId(request.params?.id);
|
|
const body = parseBody(PATCH_SCHEMA, request.body);
|
|
const updated = await fastify.prisma.cutoverScope.update({
|
|
where: { id: scopeId },
|
|
data: {
|
|
...(body.scopeType !== undefined ? { scope_type: body.scopeType } : {}),
|
|
...(body.scopeKey !== undefined ? { scope_key: body.scopeKey } : {}),
|
|
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
|
|
...(body.startsAt !== undefined ? { starts_at: body.startsAt ? new Date(body.startsAt) : null } : {}),
|
|
...(body.notes !== undefined ? { notes: body.notes ?? null } : {}),
|
|
updated_by: request.employee.id,
|
|
},
|
|
});
|
|
return sendData(reply, request, { item: mapScope(updated) });
|
|
});
|
|
}
|
|
module.exports = cutoverRoutes;
|