40227-vm/backend/docs/content-catalog.md

69 lines
6.8 KiB
Markdown

# 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.
## 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.
- 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.
- 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.
### 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`.
## Related
- Frontend: `frontend/docs/content-catalog-integration.md`.
- Related backend slice: communications (`backend/docs/communications.md`) covers internal alerts and direct guardian/staff messages.