40227-vm/backend/docs/content-catalog.md
2026-06-17 21:45:57 +02:00

8.2 KiB

Content Catalog Backend

Purpose

content_catalog stores backend-owned, seeded product content keyed by content_type, which the frontend renders through backend APIs. The database and backend seeds are the source of truth for editable/scoped domain/content records. Product-static catalogs such as personality quiz content and classroom-timer presets live in frontend constants instead. Authenticated reads return the active payload scoped to the current user, and authenticated management endpoints allow runtime configuration of catalog records.

Slice Files (by layer)

  • Routes:
    • src/routes/content_catalog.ts — authenticated read (GET /read/:contentType) plus management (GET /, POST /, GET /:contentType, PUT /:contentType, DELETE /:contentType). Mounted at /api/content-catalog behind the authenticated middleware.
  • Controllers:
    • src/api/controllers/content_catalog.controller.ts (readByType, list, create, findManagedByType, update, remove).
  • Service (BLL): src/services/content_catalog.ts (single ContentCatalogService: list, findByType, findManagedByType, create, update, delete).
  • Repository (DAL): queries run through db.content_catalog inside the service (no separate db/api file).
  • Model: src/db/models/content_catalog.ts (no model associations).
  • Shared used: db/with-transaction.ts, services/shared/access.ts (hasFeaturePermission, tenant helpers), shared/constants/content-catalog.ts, shared/constants/pagination.ts (resolvePagination), shared/errors/forbidden.ts, shared/errors/validation.ts.
  • Seeds: src/db/seeders/20260608103000-content-catalog.ts with payloads in src/db/seeders/content-catalog-data/content-catalog-seed-payloads.ts.

API

  • GET /api/content-catalog/read/:contentType -> 200 the active content DTO for that contentType, scoped to the authenticated user. Throws ValidationError('contentCatalogNotFound') when no active record exists.
  • GET /api/content-catalog -> 200 { rows, count }. JWT + manage access. Supports limit/page.
  • POST /api/content-catalog -> 201 the created DTO. JWT + manage access. Body is req.body.data.
  • GET /api/content-catalog/:contentType -> 200 the active DTO for that type. JWT + manage access (delegates to findByType).
  • PUT /api/content-catalog/:contentType -> 200 the updated DTO. JWT + manage access. Body is req.body.data.
  • DELETE /api/content-catalog/:contentType -> 204 no body. JWT + manage access.

DTO fields: id, content_type, payload, updatedAt.

Access Rules

  • GET /api/content-catalog/read/:contentType requires an authenticated user and returns only records where active = true.
  • All /api/content-catalog management endpoints require MANAGE_CONTENT_CATALOG. custom_permissions can grant it and custom_permissions_filter can remove it for non-global users. Tenant/content-type scoping still constrains which row is edited.
  • classroom-strategies is an organization-scoped catalog. Management requires MANAGE_CONTENT_CATALOG and an effective organization scope; school, campus, and class drill-down scopes can read it but cannot update the organization strategy library.

Tenant Scope

Content records can be tenant-scoped through nullable organizationId, schoolId, campusId, and classId columns:

  • Per-tenant types use the caller's own tenant (getOwnTenant) and exact owner matching. Dashboard content (dashboard-encouraging-quotes, dashboard-compliance-items, dashboard-sign-of-week, and related dashboard assets) is seeded at organization, school, and campus scope so leadership dashboards work at every supported drill-down level.
  • School-scoped types read the caller's resolved school row.
  • Org-scoped types read the caller's organization row. classroom-strategies is org-scoped: every organization gets one preset editable strategy library, while school, campus, and classroom views read that organization library instead of owning duplicate rows.
  • Shared/global types use all-null tenant ids.

Management list excludes tenant-scoped content because those records are edited through dedicated per-type editors.

Data Contract

  • Create input: content_type (required non-empty string), payload (required; any non-undefined JSON value), optional active (defaults to true unless explicitly false), optional importHash.
  • Update input: payload (required), optional active (set to true unless explicitly false).
  • Model fields: id (UUID), content_type (text, not null), payload (JSONB, not null), active (boolean, not null, default true), nullable tenant ids (organizationId, schoolId, campusId, classId), importHash (nullable, unique), createdAt, updatedAt, deletedAt. paranoid soft deletes; freezeTableName.
  • List pagination: list uses resolvePagination(limit, page) and orders by content_type asc.

Behavior / Notes

  • create looks up any existing row by content_type plus its exact tenant owner with paranoid: false. If a non-deleted row exists it throws ValidationError. If a soft-deleted row exists it is restored and updated inside withTransaction; otherwise a new row is created.
  • update and delete run inside withTransaction and throw ValidationError('contentCatalogNotFound') when the row is missing. delete sets active = false then soft-deletes (destroy).
  • findManagedByType is the authenticated variant of findByType: it enforces manage access and then returns the active record.
  • For classroom-strategies, findManagedByType, create, update, and delete also require organization effective scope so organization leaders manage the shared strategy library and descendant scopes remain read-only.
  • Managed classroom-strategies images are uploaded through the file subsystem first. The content payload stores the returned private URL; production uploads use the configured GCloud bucket path from file.md.
  • Missing/inactive content types fail explicitly with ValidationError rather than returning empty payloads.

Seeded content types

The seeder (20260608103000-content-catalog.ts) loads the following content_type keys from content-catalog-seed-payloads.ts: classroom-strategies, safety-qbs-quiz, sign-language-items, sign-language-page-content, regulation-zones, zones-of-regulation-page-content, dashboard-teacher-images, dashboard-encouraging-quotes, dashboard-compliance-items, dashboard-sign-of-week, community-organizations, vocational-opportunities, emotional-intelligence-assessment-questions, emotional-intelligence-weekly-topics, emotional-intelligence-growth-tips, emotional-intelligence-team-wellness-metrics, emotional-intelligence-weekly-focus, esa-funding-content.

The migration 20260614090000-drop-global-content-catalog-rows.ts removes the former global static rows: personality-* and classroom-timer-*. The migration 20260616170000-backfill-dashboard-content-scopes.ts backfills per-tenant dashboard catalog defaults for existing organization, school, and campus records.

New tenant creation uses ContentCatalogSeedService.seedDefaultContentForTenant: org creation presets org-scoped content such as classroom-strategies, school creation presets school-scoped content, and campus creation presets only per-tenant campus content. This keeps the Classroom Support library shared at the organization level.

Content authoring rules

  • Add editable or tenant-scoped production content records to backend seed payloads, not frontend constants.
  • Frontend constants stay limited to UI config, labels, query keys, timing values, presentation tokens, and product-static catalogs.
  • If a catalog needs complex workflow state, approvals, or per-campus variants, replace the generic catalog entry with typed, tenant-scoped backend tables and CRUD APIs.

Tests

Coverage lives in src/services/content_catalog.test.ts and src/services/content_catalog_seed.test.ts. It covers tenant read scoping, organization-only management for classroom-strategies, and organization-only seeding for the preset Classroom Support library.

  • Frontend: frontend/docs/content-catalog-integration.md.
  • Related backend slice: communications (backend/docs/communications.md) covers internal alerts and direct guardian/staff messages.