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-cataloginsrc/index.ts(NOT behind theauthenticatedmiddleware).src/routes/content_catalog.ts— management (GET /,POST /,GET /:contentType,PUT /:contentType,DELETE /:contentType). Mounted at/api/content-catalogbehind theauthenticatedmiddleware.
- 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(singleContentCatalogService:list,findByType,findManagedByType,create,update,delete). - Repository (DAL): queries run through
db.content_cataloginside the service (no separatedb/apifile). - 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.tswith payloads insrc/db/seeders/content-catalog-data/content-catalog-seed-payloads.ts.
API
GET /api/public/content-catalog/:contentType->200the active content DTO for thatcontentType. No JWT required. ThrowsValidationError('contentCatalogNotFound')when no active record exists.GET /api/content-catalog->200{ rows, count }. JWT + manage access. Supportslimit/page.POST /api/content-catalog->201the created DTO. JWT + manage access. Body isreq.body.data.GET /api/content-catalog/:contentType->200the active DTO for that type. JWT + manage access (delegates tofindByType).PUT /api/content-catalog/:contentType->200the updated DTO. JWT + manage access. Body isreq.body.data.DELETE /api/content-catalog/:contentType->204no 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 whereactive = true. - All
/api/content-catalogmanagement endpoints require manage access (assertCanManageContentCatalog): the user must hold one ofCONTENT_CATALOG_MANAGER_ROLE_NAMES(super admin, admin, platform owner, tenant director, campus manager) or have global access; otherwiseForbiddenError. (hasRoleAccessis the only gate; there is no separateassertAuthenticatedTenantUsercall 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-undefinedJSON value), optionalactive(defaults totrueunless explicitlyfalse), optionalimportHash. - Update input:
payload(required), optionalactive(set totrueunless explicitlyfalse). - Model fields:
id(UUID),content_type(text, unique, not null),payload(JSONB, not null),active(boolean, not null, defaulttrue),importHash(nullable, unique),createdAt,updatedAt,deletedAt.paranoidsoft deletes;freezeTableName. - List pagination:
listusesresolvePagination(limit, page)and orders bycontent_type asc.
Behavior / Notes
createlooks up any existing row bycontent_typewithparanoid: false. If a non-deleted row exists it throwsValidationError. If a soft-deleted row exists it is restored and updated insidewithTransaction; otherwise a new row is created.updateanddeleterun insidewithTransactionand throwValidationError('contentCatalogNotFound')when the row is missing.deletesetsactive = falsethen soft-deletes (destroy).findManagedByTypeis the authenticated variant offindByType: it enforces manage access and then returns the active record.- Missing/inactive content types fail explicitly with
ValidationErrorrather 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).
Related
- Frontend:
frontend/docs/content-catalog-integration.md. - Related backend slice: communications (
backend/docs/communications.md) — its UI consumesparent-message-templatesandsafety-protocolsfrom this catalog.