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