forked from bai/curriculum-project-hub
feat: org capacity policy admin surface
ADR-0022 / Spec.System.Capacity pins layered capacity limits: a platform ceiling per dimension is unbreakable, and each organization may only set a lower `organizationLimit`. The effective limit is the minimum of the two (`LayeredLimit.effective`); dimensions with no org override fall back to the platform ceiling. No dimension may be unlimited (a ceiling must exist before an org limit can be set, `LayeredLimit.Valid`). Add the backend: the pinned 23 CapacityDimension set + labels (src/capacity/dimensions.ts), platform ceilings sourced from existing runtime env vars plus `HUB_CEILING_<DIMENSION>` (src/capacity/ceilings.ts), the OrganizationCapacityPolicy prisma model + migration, the getCapacityPolicy/setCapacityPolicy service enforcing LayeredLimit.Valid, and org-admin GET/PUT /api/org/:orgSlug/capacity-policy routes wired into the org route tree. Add the admin-web surface: CapacityDimension/CapacityPolicyView api client types, capacityPolicy/setCapacityPolicy methods, a `容量` nav entry, and a capacity page that lists every dimension with its platform ceiling (or `未配置` when unset), an org-limit input (disabled until a ceiling exists), and a live effective-value column. Saving sends the partial limits map; the service rejects values above the ceiling or for unconfigured dimensions.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Org capacity policy service (ADR-0022 / spec `Spec.System.Capacity`).
|
||||
*
|
||||
* Stores per-Organization lower `organizationLimit` overrides per
|
||||
* `CapacityDimension`. Enforces `LayeredLimit.Valid`: a set limit must be ≤ the
|
||||
* platform ceiling for that dimension. `LayeredLimit.effective` (min of the two)
|
||||
* is the value capacity admission should use; dimensions with no org override
|
||||
* fall back to the platform ceiling.
|
||||
*/
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { CAPACITY_DIMENSIONS, isCapacityDimension, type CapacityDimension } from "../capacity/dimensions.js";
|
||||
import { readPlatformCeilings } from "../capacity/ceilings.js";
|
||||
import { lockActiveOrganization } from "./status.js";
|
||||
|
||||
export interface CapacityPolicyView {
|
||||
/** Per-dimension: platform ceiling (absent if not configured) + org limit. */
|
||||
readonly dimensions: ReadonlyArray<{
|
||||
readonly dimension: CapacityDimension;
|
||||
readonly platformCeiling: number | null;
|
||||
readonly organizationLimit: number | null;
|
||||
readonly effective: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function getCapacityPolicy(
|
||||
prisma: PrismaClient,
|
||||
organizationId: string,
|
||||
): Promise<CapacityPolicyView> {
|
||||
const row = await prisma.organizationCapacityPolicy.findUnique({
|
||||
where: { organizationId },
|
||||
select: { limits: true },
|
||||
});
|
||||
const orgLimits = parseLimits(row?.limits);
|
||||
const ceilings = readPlatformCeilings();
|
||||
const dimensions = CAPACITY_DIMENSIONS.map((dimension) => {
|
||||
const platformCeiling = ceilings[dimension] ?? null;
|
||||
const organizationLimit = orgLimits[dimension] ?? null;
|
||||
const effective = effectiveLimit(platformCeiling, organizationLimit);
|
||||
return { dimension, platformCeiling, organizationLimit, effective };
|
||||
});
|
||||
return { dimensions };
|
||||
}
|
||||
|
||||
export async function setCapacityPolicy(
|
||||
prisma: PrismaClient,
|
||||
input: {
|
||||
readonly organizationId: string;
|
||||
/** Partial map of dimension → limit. null/absent clears a dimension. */
|
||||
readonly limits: Partial<Record<CapacityDimension, number | null>>;
|
||||
},
|
||||
): Promise<CapacityPolicyView> {
|
||||
const ceilings = readPlatformCeilings();
|
||||
const cleaned: Record<string, number> = {};
|
||||
for (const [dimension, value] of Object.entries(input.limits)) {
|
||||
if (!isCapacityDimension(dimension)) {
|
||||
throw new Error(`unknown capacity dimension: ${dimension}`);
|
||||
}
|
||||
if (value === null || value === undefined) continue;
|
||||
if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
|
||||
throw new Error(`limit for ${dimension} must be a positive integer`);
|
||||
}
|
||||
const ceiling = ceilings[dimension];
|
||||
if (ceiling === undefined) {
|
||||
throw new Error(
|
||||
`platform ceiling for ${dimension} is not configured; cannot set an organization limit (spec LayeredLimit.Valid)`,
|
||||
);
|
||||
}
|
||||
if (value > ceiling) {
|
||||
throw new Error(
|
||||
`organization limit ${value} for ${dimension} exceeds platform ceiling ${ceiling} (spec LayeredLimit.Valid)`,
|
||||
);
|
||||
}
|
||||
cleaned[dimension] = value;
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await lockActiveOrganization(tx, input.organizationId);
|
||||
await tx.organizationCapacityPolicy.upsert({
|
||||
where: { organizationId: input.organizationId },
|
||||
create: { organizationId: input.organizationId, limits: cleaned },
|
||||
update: { limits: cleaned },
|
||||
});
|
||||
});
|
||||
return getCapacityPolicy(prisma, input.organizationId);
|
||||
}
|
||||
|
||||
function effectiveLimit(platformCeiling: number | null, organizationLimit: number | null): number | null {
|
||||
if (platformCeiling === null) return null;
|
||||
if (organizationLimit === null) return platformCeiling;
|
||||
return Math.min(platformCeiling, organizationLimit);
|
||||
}
|
||||
|
||||
function parseLimits(raw: unknown): Partial<Record<CapacityDimension, number>> {
|
||||
if (raw === null || typeof raw !== "object") return {};
|
||||
const result: Partial<Record<CapacityDimension, number>> = {};
|
||||
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
||||
if (isCapacityDimension(key) && typeof value === "number" && Number.isFinite(value)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user