forked from bai/curriculum-project-hub
63 lines
2.2 KiB
TypeScript
63 lines
2.2 KiB
TypeScript
import type { OrganizationStatus, Prisma } from "@prisma/client";
|
|
|
|
export class InactiveOrganizationError extends Error {
|
|
readonly organizationId: string;
|
|
readonly status: Exclude<OrganizationStatus, "ACTIVE">;
|
|
|
|
constructor(organizationId: string, status: Exclude<OrganizationStatus, "ACTIVE">) {
|
|
super(`organization ${organizationId} is ${status}; customer operations require ACTIVE`);
|
|
this.name = "InactiveOrganizationError";
|
|
this.organizationId = organizationId;
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export function inactiveOrganizationReason(
|
|
organizationId: string,
|
|
status: OrganizationStatus,
|
|
): string | undefined {
|
|
if (status === "ACTIVE") return undefined;
|
|
return `organization ${organizationId} is ${status}; customer operations require ACTIVE`;
|
|
}
|
|
|
|
export function requireActiveOrganizationStatus(
|
|
organizationId: string,
|
|
status: OrganizationStatus,
|
|
): void {
|
|
if (status !== "ACTIVE") throw new InactiveOrganizationError(organizationId, status);
|
|
}
|
|
|
|
/**
|
|
* Lock the Organization row for a customer operation and verify ACTIVE.
|
|
* PostgreSQL UPDATE takes a conflicting row lock, so lifecycle transitions
|
|
* serialize with the entire transaction that called this function.
|
|
*/
|
|
export async function lockActiveOrganization(
|
|
prisma: Prisma.TransactionClient,
|
|
organizationId: string,
|
|
): Promise<void> {
|
|
const rows = await prisma.$queryRaw<Array<{ readonly id: string; readonly status: OrganizationStatus }>>`
|
|
SELECT "id", "status"::text AS "status"
|
|
FROM "Organization"
|
|
WHERE "id" = ${organizationId}
|
|
FOR SHARE
|
|
`;
|
|
const organization = rows[0];
|
|
if (organization === undefined) throw new Error(`organization not found: ${organizationId}`);
|
|
requireActiveOrganizationStatus(organization.id, organization.status);
|
|
}
|
|
|
|
/** Resolve a Project's tenant inside the transaction, then take the lifecycle lock. */
|
|
export async function lockActiveProjectOrganization(
|
|
prisma: Prisma.TransactionClient,
|
|
projectId: string,
|
|
): Promise<string> {
|
|
const project = await prisma.project.findUnique({
|
|
where: { id: projectId },
|
|
select: { organizationId: true },
|
|
});
|
|
if (project === null) throw new Error(`project not found: ${projectId}`);
|
|
await lockActiveOrganization(prisma, project.organizationId);
|
|
return project.organizationId;
|
|
}
|