114 lines
3.3 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|