40227-vm/backend/src/services/content_catalog_seed.ts
2026-06-18 10:09:11 +02:00

114 lines
3.3 KiB
TypeScript

import type { Transaction } from 'sequelize';
import db from '@/db/models';
import {
PER_TENANT_CONTENT_TYPES,
SCHOOL_SCOPED_CONTENT_TYPES,
ORG_SCOPED_CONTENT_TYPES,
} from '@/shared/constants/content-catalog';
import { CONTENT_CATALOG_DEFAULT_ROWS } from '@/db/seeders/content-catalog-data/content-catalog-seed-payloads';
export type TenantSeedLevel = 'organization' | 'school' | 'campus';
export interface TenantSeedContext {
level: TenantSeedLevel;
organizationId: string;
schoolId?: string | null;
campusId?: string | null;
}
interface OwnerStamp {
organizationId: string | null;
schoolId: string | null;
campusId: string | null;
}
/**
* The owning-tenant ids a content type takes when the given tenant level is
* created — or `null` if this type is not preset at this level. Per-tenant
* org-scoped content such as the safety quiz exists only at organization level;
* dashboard + parent templates exist at org/school/campus; school-scoped only
* at school; truly global types are seeded once (with no tenant) when the first
* org is created.
*/
function stampForLevel(
contentType: string,
ctx: TenantSeedContext,
): OwnerStamp | null {
const org = ctx.organizationId;
if (ORG_SCOPED_CONTENT_TYPES.has(contentType)) {
return ctx.level === 'organization'
? { organizationId: org, schoolId: null, campusId: null }
: null;
}
if (SCHOOL_SCOPED_CONTENT_TYPES.has(contentType)) {
return ctx.level === 'school' && ctx.schoolId
? { organizationId: org, schoolId: ctx.schoolId, campusId: null }
: null;
}
if (PER_TENANT_CONTENT_TYPES.has(contentType)) {
if (ctx.level === 'organization') {
return { organizationId: org, schoolId: null, campusId: null };
}
if (ctx.level === 'school') {
return ctx.schoolId
? { organizationId: org, schoolId: ctx.schoolId, campusId: null }
: null;
}
return ctx.campusId
? { organizationId: org, schoolId: null, campusId: ctx.campusId }
: null;
}
// Truly global/shared content: seeded once (no tenant) at org creation.
return ctx.level === 'organization'
? { organizationId: null, schoolId: null, campusId: null }
: null;
}
/**
* Copies the default content a newly-created tenant owns into `content_catalog`,
* once (idempotent — skips rows that already exist for that tenant). Called from
* the org/school/campus creation flows so new tenants start with editable
* defaults from the single defaults source.
*/
export async function seedDefaultContentForTenant(
ctx: TenantSeedContext,
transaction?: Transaction,
): Promise<void> {
for (const row of CONTENT_CATALOG_DEFAULT_ROWS) {
const stamp = stampForLevel(row.content_type, ctx);
if (!stamp) {
continue;
}
const where = {
content_type: row.content_type,
organizationId: stamp.organizationId,
schoolId: stamp.schoolId,
campusId: stamp.campusId,
classId: null,
};
const existing = await db.content_catalog.findOne({
where,
paranoid: false,
transaction,
});
if (existing) {
continue;
}
await db.content_catalog.create(
{
content_type: row.content_type,
payload: row.payload,
active: true,
importHash: null,
...stamp,
classId: null,
},
{ transaction },
);
}
}