Daily programming — data model
Schema in libs/db/src/lib/schema/scheduling.ts.
daily_programming
| Column | Type | Meaning |
|---|---|---|
id | uuid PK | |
class_type_id | uuid FK → class_types.id, not null | The slot’s class type. |
date | date (string mode), not null | The slot’s date. |
workout_id | uuid FK → workouts.id, not null | The library workout. Snapshots are not used here (broadcast surface). |
sort_order | integer default 0 | When multiple workouts on the same slot. |
status | varchar(20) default 'draft' | draft / published. Not a DB enum; service / DTO enforces values. |
created_by | uuid FK → users.id, not null | |
created_at / updated_at | timestamps | No deleted_at — deletes are physical. |
Indexes / constraints:
- UNIQUE
(class_type_id, date, workout_id). Prevents the same library workout being added twice to the same slot. (Implementation in the migration; visible in the schema asunique().on(table.classTypeId, table.date, table.workoutId).)
There is no (class_type_id, date)-only index — the hot query is the week range scan, which uses date ordering directly. As the table grows, adding (class_type_id, date) may be worthwhile.
Row lifecycle
| Event | DB effect |
|---|---|
PUT /daily-programming | Inside a transaction: DELETE rows matching (class_type_id, date), then INSERT new rows for the supplied workouts. Status defaults to draft. |
PATCH /status | UPDATE status for all rows matching (class_type_id, date). |
DELETE /daily-programming | Physical DELETE for (class_type_id, date). No tombstone. |
applyToSchedule (template) | Bulk INSERT (one row per workout cell that lands on an active day). Always status='draft'. |
Soft-delete vs hard-delete
This is the only feature in the workouts domain that uses physical delete. There is no deleted_at column. Removing a slot is destructive; previous content is not recoverable through the API. (Audit-log instrumentation would be the next step if accountability becomes a requirement.)
Multi-org isolation
- No
organization_idcolumn. Org scoping is done viaclass_types.programId → programs.organizationId. getOrgClassTypeIds(orgId)resolves the org’s class type ids in one SQL join and thenWHERE class_type_id IN (...)scopes the read.- Every write goes through
verifyClassTypeBelongsToOrg, which independently re-verifies the class type’s org. Cross-org writes are blocked at the API boundary. workoutIds are independently verified againstworkouts.organizationId = orgId.- Members’ visibility filter (only
status='published') is applied JS-side after the SQL fetch.