40227-vm/backend/docs/content-catalog.md
2026-06-10 18:27:19 +02:00

6.4 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 these domain/content records, instead of duplicating them in frontend runtime constants. A public read endpoint serves the active payload for a content type, and authenticated management endpoints allow runtime configuration of catalog records.

Slice Files (by layer)

  • Routes:
    • src/routes/public_content_catalog.ts — public read (GET /:contentType). Mounted at /api/public/content-catalog in src/index.ts (NOT behind the authenticated middleware).
    • src/routes/content_catalog.ts — management (GET /, POST /, GET /:contentType, PUT /:contentType, DELETE /:contentType). Mounted at /api/content-catalog behind the authenticated middleware.
  • Controllers:
    • src/api/controllers/public_content_catalog.controller.ts (findByType).
    • src/api/controllers/content_catalog.controller.ts (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 (hasRoleAccess), shared/constants/content-catalog.ts (CONTENT_CATALOG_MANAGER_ROLE_NAMES), 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/public/content-catalog/:contentType -> 200 the active content DTO for that contentType. No JWT required. 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

  • The public read endpoint (/api/public/content-catalog/:contentType) is unauthenticated and applies no role check; it only returns records where active = true.
  • All /api/content-catalog management endpoints require manage access (assertCanManageContentCatalog): the user must hold one of CONTENT_CATALOG_MANAGER_ROLE_NAMES (super admin, admin, platform owner, tenant director, campus manager) or have global access; otherwise ForbiddenError. (hasRoleAccess is the only gate; there is no separate assertAuthenticatedTenantUser call in this service.)

Tenant Scope

None. content_catalog has no organizationId/campusId columns and the service applies no tenant or campus filtering; records are global. content_type is unique across the table.

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, unique, not null), payload (JSONB, not null), active (boolean, not null, default true), 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 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.
  • 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, parent-message-templates, 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, personality-quiz-questions, personality-types, personality-workplace-content, esa-funding-content, safety-protocols, classroom-timer-backgrounds, classroom-timer-sounds, classroom-timer-presets, classroom-timer-tips, personality-quiz-features.

Content authoring rules

  • Add production content records to backend seed payloads, not frontend constants.
  • Frontend constants stay limited to UI config, labels, query keys, timing values, and presentation tokens.
  • 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

None yet (no *.test.ts under backend/src references this slice).

  • Frontend: frontend/docs/content-catalog-integration.md.
  • Related backend slice: communications (backend/docs/communications.md) — its UI consumes parent-message-templates and safety-protocols from this catalog.