From 52524eb72dba29ca6b8b1bd6a2d1178488379def Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 2 May 2026 09:55:10 +0200 Subject: [PATCH] =?UTF-8?q?Task=201.5=20=E2=80=94=20Event-participation=20?= =?UTF-8?q?collections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five collections + 10 relations + 5 composite unique constraints, captured into snapshots/schema.yaml (now 105 KB, up from 53 KB). Collections: - events — 11 fields incl. organization_id M2O, discipline enum (rally / time-trial / regatta / trail-run / hike), starts_at/ends_at required. - classes — 8 fields incl. event_id M2O, code unique within event. - entries — 11 fields incl. event_id/vehicle_id (nullable for foot races) /class_id M2O, race_number, status enum with 8 lifecycle values, archive on `withdrawn`. team_id deliberately omitted (Phase 2+). - entry_crew — junction with role enum (pilot/co-pilot/navigator/mechanic/rider/runner/hiker). - entry_devices — junction with optional assigned_user_id (panic button body-wear); ON DELETE SET NULL on that field since user removal shouldn't block the device record. 10 M2O relations wired, all ON DELETE RESTRICT except entry_devices.assigned_user_id (SET NULL). db-init/005_event_participation_unique_constraints.sql adds composite UNIQUE on: events (organization_id, slug) classes (event_id, code) entries (event_id, race_number) entry_crew (entry_id, user_id) entry_devices (entry_id, device_id) --- Destructive-apply incident (recovered): First attempt at this task hit a real foot-gun. After creating the 5 collections via MCP, we ran `compose build && up -d`. The image rebuild baked in the snapshot from task 1.4 (only 7 collections). Boot's schema-apply step ran `directus schema apply --yes` against that stale snapshot — saw the 5 new collections in the DB but not in the snapshot — DELETED THEM, taking the constraints with them. Recovery: re-created the 5 collections + 10 relations via MCP, ran the ALTER TABLE statements directly via psql to restore the constraints, ran schema:snapshot BEFORE any further restart so the YAML reflects the live state. Documented the operator rule (never rebuild with uncommitted schema changes) inline in the task spec and in the directus wiki entity page (separate commit in trm/docs). Phase 3 hardening on the radar: DIRECTUS_SCHEMA_APPLY_MODE env var with auto/dry-run/skip modes so dev environments default to non- destructive behavior. ROADMAP marks 1.5 done. Phase 1 progress: 7/9 tasks complete (1.1–1.7); 1.8, 1.9 remain. --- .planning/ROADMAP.md | 4 +- .../05-event-participation-collections.md | 68 +- ...event_participation_unique_constraints.sql | 65 + snapshots/schema.yaml | 2112 +++++++++++++++++ 4 files changed, 2246 insertions(+), 3 deletions(-) create mode 100644 db-init/005_event_participation_unique_constraints.sql diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7dfed58..9c36eea 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -42,7 +42,7 @@ These rules govern every task. Any deviation must be discussed and documented as ### Phase 1 — Slice 1 schema + deploy pipeline -**Status:** 🟨 In progress (1.1, 1.2, 1.3, 1.4, 1.6, 1.7 done; 1.5, 1.8, 1.9 remaining) +**Status:** 🟨 In progress (1.1–1.7 done; 1.8, 1.9 remaining) **Outcome:** A Directus instance with the org-level catalog (orgs, users, organization_users, vehicles, devices and their org junctions) and event-participation collections (events, classes, entries, entry_crew, entry_devices) live and snapshot-tracked. `db-init/` covers the TimescaleDB extension, the `positions` hypertable, and the `faulty` column. Image builds via Gitea Actions with a CI dry-run that catches snapshot drift before deploy. Rally Albania 2026 is registered as the first event in admin UI to dogfood the registration workflow. **This is what Rally Albania 2026 needs.** [**See `phase-1-slice-1-schema/README.md`**](./phase-1-slice-1-schema/README.md) @@ -53,7 +53,7 @@ These rules govern every task. Any deviation must be discussed and documented as | 1.2 | [db-init runner script](./phase-1-slice-1-schema/02-db-init-runner.md) | 🟩 | pending user commit | | 1.3 | [Initial migrations (extensions, positions hypertable, faulty column)](./phase-1-slice-1-schema/03-initial-migrations.md) | 🟩 | pending user commit | | 1.4 | [Org-level catalog collections](./phase-1-slice-1-schema/04-org-catalog-collections.md) | 🟩 | pending user commit | -| 1.5 | [Event-participation collections](./phase-1-slice-1-schema/05-event-participation-collections.md) | ⬜ | — | +| 1.5 | [Event-participation collections](./phase-1-slice-1-schema/05-event-participation-collections.md) | 🟩 | pending user commit | | 1.6 | [Schema snapshot/apply tooling](./phase-1-slice-1-schema/06-snapshot-tooling.md) | 🟩 | pending user commit | | 1.7 | [Image build & entrypoint](./phase-1-slice-1-schema/07-image-and-dockerfile.md) | 🟩 | pending user commit | | 1.8 | [Gitea CI dry-run workflow](./phase-1-slice-1-schema/08-gitea-ci-dryrun.md) | ⬜ | — | diff --git a/.planning/phase-1-slice-1-schema/05-event-participation-collections.md b/.planning/phase-1-slice-1-schema/05-event-participation-collections.md index f7dd8ca..9ac66ed 100644 --- a/.planning/phase-1-slice-1-schema/05-event-participation-collections.md +++ b/.planning/phase-1-slice-1-schema/05-event-participation-collections.md @@ -122,4 +122,70 @@ Unique constraint: `(entry_id, device_id)` — a device can't appear twice in th ## Done -(Fill in commit SHA + one-line note when this lands.) +**Implementation landed and live-verified 2026-05-02.** All 5 collections live, snapshot grew from 53 KB to 105 KB. + +**Created (via the directus-local MCP server, same approach as 1.4):** +- `events` — 11 fields incl. organization_id M2O, discipline enum (rally/time-trial/regatta/trail-run/hike), starts_at/ends_at required. +- `classes` — 8 fields incl. event_id M2O, code unique within event. +- `entries` — 11 fields incl. event_id/vehicle_id (nullable)/class_id M2O, race_number, status enum with 8 values, archive on `withdrawn`. **`team_id` deliberately NOT included** per spec note (defer until Phase 2 if real team relationship is needed). +- `entry_crew` — 6 fields incl. entry_id/user_id M2O, role enum (pilot/co-pilot/navigator/mechanic/rider/runner/hiker). +- `entry_devices` — 7 fields incl. entry_id/device_id M2O, assigned_user_id (nullable, `ON DELETE SET NULL` since user removal shouldn't block device record). + +**10 relations** wired across the 5 collections, all `ON DELETE RESTRICT` except `entry_devices.assigned_user_id` (`SET NULL`, deviation noted above). + +**Composite unique constraints landed via `db-init/005_event_participation_unique_constraints.sql`:** +- `events (organization_id, slug)` +- `classes (event_id, code)` +- `entries (event_id, race_number)` +- `entry_crew (entry_id, user_id)` +- `entry_devices (entry_id, device_id)` + +--- + +**⚠️ Schema-apply destructive deletion incident (2026-05-02):** + +This task surfaced a real foot-gun in our boot pipeline. Documenting in detail so future work avoids it. + +**What happened:** + +1. We created 5 new collections via MCP against the running Directus. +2. We then ran `docker compose build && up -d` to make `db-init/005_*.sql` apply. +3. The image rebuild baked in the OLD `snapshots/schema.yaml` (committed in task 1.4 — only had 7 collections). +4. Boot ran the entrypoint chain. db-init applied 005 successfully (constraints landed on the new tables). But step 2/4 (`schema-apply.sh` → `directus schema apply --yes /directus/snapshots/schema.yaml`) compared the running DB against the stale snapshot and saw 5 collections that "shouldn't exist" — so it **deleted them**, taking the constraints with them. +5. End state: 5 collections gone, db-init/005 row in `migrations_applied` still recorded as applied (so it wouldn't re-run), production-shape damage in dev. + +**Why `directus schema apply --yes` is destructive by design:** + +The `--yes` flag tells Directus to enforce the snapshot as the single source of truth — anything in the DB but not in the snapshot is dropped. This is the *correct* behavior for fresh-environment provisioning (tasks 1.7's entrypoint, 1.8's CI dry-run, prod boots) where the snapshot IS the canonical state. It is the *wrong* behavior during active schema development when the snapshot lags behind live changes. + +**Recovery performed:** + +1. Re-created the 5 collections + 10 relations via MCP (same calls as the original task 1.5 work — repeatable since the data was source-controlled in the conversation). +2. Re-applied the 5 ALTER TABLE statements from `db-init/005_*.sql` directly via psql (since `migrations_applied` already had 005 recorded). +3. Ran `pnpm run schema:snapshot` *before* any further restart. Snapshot now reflects the full 13-collection state. + +**Discipline going forward (operator rule):** + +> **Never restart or rebuild the Directus container while there are uncommitted schema changes.** The flow is always: change in admin UI / via MCP → `pnpm run schema:snapshot` → commit → only then rebuild/restart. + +This rule is now documented in `wiki/entities/directus.md` Schema management section. + +**Architectural follow-up (not for Phase 1):** + +The entrypoint's hard-coded `--yes` is a long-term issue. Phase 3 hardening could introduce a `DIRECTUS_SCHEMA_APPLY_MODE` env var with values `auto` (current behavior, prod default), `dry-run` (log diff only, halt on drift — dev default), `skip`. Tracked as a Phase 3 task; non-blocking for slice-1 ship. + +--- + +**Acceptance criteria status:** + +- ✅ All 5 collections exist with the fields specified. +- ✅ Required fields flagged (events.organization_id/name/slug/discipline/starts_at/ends_at, classes.event_id/code/name, entries.event_id/class_id/race_number/status, entry_crew.entry_id/user_id/role, entry_devices.entry_id/device_id). +- ✅ Single-column unique constraints — none in this task (all uniqueness is composite). +- ✅ Composite unique constraints (5 of them) enforced via db-init/005. +- ✅ M2O relations wired (10 total). +- ✅ status enum dropdown shows all 8 values in lifecycle order. +- ✅ race_number is integer. +- ✅ team_id field omitted per spec note. +- ✅ No permission policies attached. +- ✅ `pnpm run schema:snapshot` produces snapshots/schema.yaml with all 5 new collections. +- ⏳ End-to-end test (manually create event → class → entry → entry_crew → entry_devices) — pending user. diff --git a/db-init/005_event_participation_unique_constraints.sql b/db-init/005_event_participation_unique_constraints.sql new file mode 100644 index 0000000..2b54b38 --- /dev/null +++ b/db-init/005_event_participation_unique_constraints.sql @@ -0,0 +1,65 @@ +-- 005_event_participation_unique_constraints.sql +-- Composite UNIQUE constraints on the event-participation collections. +-- +-- Same rationale as 004: Directus's `is_unique` flag is single-column only; +-- composite uniqueness lives in db-init/ because the snapshot YAML format +-- does not capture multi-column unique constraints. +-- +-- Owned by: task 1.5 (event-participation collections). + +ALTER TABLE events + ADD CONSTRAINT events_org_slug_unique + UNIQUE (organization_id, slug); + +ALTER TABLE classes + ADD CONSTRAINT classes_event_code_unique + UNIQUE (event_id, code); + +ALTER TABLE entries + ADD CONSTRAINT entries_event_race_number_unique + UNIQUE (event_id, race_number); + +ALTER TABLE entry_crew + ADD CONSTRAINT entry_crew_entry_user_unique + UNIQUE (entry_id, user_id); + +ALTER TABLE entry_devices + ADD CONSTRAINT entry_devices_entry_device_unique + UNIQUE (entry_id, device_id); + +-- ------------------------------------------------------------------------- +-- Assertion block: verify all five constraints landed. +-- ------------------------------------------------------------------------- +DO $$ BEGIN + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'events_org_slug_unique' + ) THEN + RAISE EXCEPTION 'events composite unique constraint (org, slug) missing'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'classes_event_code_unique' + ) THEN + RAISE EXCEPTION 'classes composite unique constraint (event, code) missing'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'entries_event_race_number_unique' + ) THEN + RAISE EXCEPTION 'entries composite unique constraint (event, race_number) missing'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'entry_crew_entry_user_unique' + ) THEN + RAISE EXCEPTION 'entry_crew composite unique constraint (entry, user) missing'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'entry_devices_entry_device_unique' + ) THEN + RAISE EXCEPTION 'entry_devices composite unique constraint (entry, device) missing'; + END IF; + +END $$; diff --git a/snapshots/schema.yaml b/snapshots/schema.yaml index a61e3bd..65aaaaa 100644 --- a/snapshots/schema.yaml +++ b/snapshots/schema.yaml @@ -2,6 +2,32 @@ version: 1 directus: 11.17.4 vendor: postgres collections: + - collection: classes + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: classes + color: null + display_template: '{{code}} — {{name}}' + group: null + hidden: false + icon: category + item_duplication_fields: null + note: >- + Per-event classification (e.g. M-1, C-2, S-3 in Rally Albania). Class + drives standings grouping. + preview_url: null + singleton: false + sort: 8 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: classes - collection: devices meta: accountability: all @@ -28,6 +54,110 @@ collections: versioning: false schema: name: devices + - collection: entries + meta: + accountability: all + archive_app_filter: true + archive_field: status + archive_value: withdrawn + collapse: open + collection: entries + color: null + display_template: '#{{race_number}} — {{class_id.code}}' + group: null + hidden: false + icon: how_to_reg + item_duplication_fields: null + note: >- + The unit of timing. One row per (vehicle or solo participant) registered + for an event. team_id deferred until Phase 2. + preview_url: null + singleton: false + sort: 9 + sort_field: null + translations: null + unarchive_value: registered + versioning: false + schema: + name: entries + - collection: entry_crew + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: entry_crew + color: null + display_template: '{{user_id.first_name}} {{user_id.last_name}} — {{role}}' + group: null + hidden: false + icon: groups + item_duplication_fields: null + note: >- + Junction: which users participate in which entries with what racing + role. + preview_url: null + singleton: false + sort: 10 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: entry_crew + - collection: entry_devices + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: entry_devices + color: null + display_template: '{{device_id.imei}} — {{mount_position}}' + group: null + hidden: false + icon: sensors + item_duplication_fields: null + note: >- + Junction: which devices are mounted on which entry, optionally body-worn + on a specific crew member. + preview_url: null + singleton: false + sort: 11 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: entry_devices + - collection: events + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: events + color: null + display_template: '{{name}}' + group: null + hidden: false + icon: event + item_duplication_fields: null + note: >- + An event = a single rally / time-trial / regatta / etc. Lives in exactly + one organization. The container for classes, entries, course definition. + preview_url: null + singleton: false + sort: 7 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: events - collection: migrations_applied meta: accountability: all @@ -203,6 +333,333 @@ collections: schema: name: vehicles fields: + - collection: classes + field: id + type: uuid + meta: + collection: classes + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: + - uuid + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: classes + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: classes + field: event_id + type: uuid + meta: + collection: classes + conditions: null + display: null + display_options: null + field: event_id + group: null + hidden: false + interface: select-dropdown-m2o + note: Event this class belongs to + options: + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: event_id + table: classes + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: events + foreign_key_column: id + - collection: classes + field: code + type: string + meta: + collection: classes + conditions: null + display: null + display_options: null + field: code + group: null + hidden: false + interface: input + note: >- + Short code (e.g. "M-1", "C-2"). Unique within the event (composite + unique via db-init/005). + options: null + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: code + table: classes + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: classes + field: name + type: string + meta: + collection: classes + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: Human-readable name (e.g. "MOTO Under 450cc") + options: null + readonly: false + required: true + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: name + table: classes + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: classes + field: description + type: text + meta: + collection: classes + conditions: null + display: null + display_options: null + field: description + group: null + hidden: false + interface: input-multiline + note: Eligibility rules in plain text + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: description + table: classes + data_type: text + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: classes + field: sort_order + type: integer + meta: + collection: classes + conditions: null + display: null + display_options: null + field: sort_order + group: null + hidden: false + interface: input + note: Display ordering within event + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: sort_order + table: classes + data_type: integer + default_value: null + max_length: null + numeric_precision: 32 + numeric_scale: 0 + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: classes + field: date_created + type: timestamp + meta: + collection: classes + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: classes + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: classes + field: date_updated + type: timestamp + meta: + collection: classes + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 8 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: classes + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null - collection: devices field: id type: uuid @@ -611,6 +1068,1451 @@ fields: has_auto_increment: false foreign_key_table: null foreign_key_column: null + - collection: entries + field: id + type: uuid + meta: + collection: entries + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: + - uuid + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: entries + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entries + field: event_id + type: uuid + meta: + collection: entries + conditions: null + display: null + display_options: null + field: event_id + group: null + hidden: false + interface: select-dropdown-m2o + note: Event this entry races in + options: + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: event_id + table: entries + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: events + foreign_key_column: id + - collection: entries + field: vehicle_id + type: uuid + meta: + collection: entries + conditions: null + display: null + display_options: null + field: vehicle_id + group: null + hidden: false + interface: select-dropdown-m2o + note: 'Nullable: null for foot races (trail-run / hike disciplines)' + options: + template: '{{make}} {{model}}' + readonly: false + required: false + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: vehicle_id + table: entries + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: vehicles + foreign_key_column: id + - collection: entries + field: class_id + type: uuid + meta: + collection: entries + conditions: null + display: null + display_options: null + field: class_id + group: null + hidden: false + interface: select-dropdown-m2o + note: Class this entry competes in + options: + template: '{{code}} — {{name}}' + readonly: false + required: true + searchable: true + sort: 4 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: class_id + table: entries + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: classes + foreign_key_column: id + - collection: entries + field: race_number + type: integer + meta: + collection: entries + conditions: null + display: null + display_options: null + field: race_number + group: null + hidden: false + interface: input + note: >- + Per Rally Albania §5: 1–199 moto, 2xx quad, 3xx car, 4xx SSV. Unique + within event (composite unique via db-init/005). + options: null + readonly: false + required: true + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: race_number + table: entries + data_type: integer + default_value: null + max_length: null + numeric_precision: 32 + numeric_scale: 0 + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entries + field: status + type: string + meta: + collection: entries + conditions: null + display: null + display_options: null + field: status + group: null + hidden: false + interface: select-dropdown + note: >- + Lifecycle: registered → confirmed → started → finished. + dnf/dns/dq/withdrawn are terminal failure states. + options: + choices: + - text: Registered + value: registered + - text: Confirmed + value: confirmed + - text: Started + value: started + - text: Finished + value: finished + - text: DNF (Did Not Finish) + value: dnf + - text: DNS (Did Not Start) + value: dns + - text: DQ (Disqualified) + value: dq + - text: Withdrawn + value: withdrawn + readonly: false + required: true + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: status + table: entries + data_type: character varying + default_value: registered + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entries + field: registered_at + type: timestamp + meta: + collection: entries + conditions: null + display: null + display_options: null + field: registered_at + group: null + hidden: false + interface: datetime + note: When the entry was registered + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: registered_at + table: entries + data_type: timestamp with time zone + default_value: CURRENT_TIMESTAMP + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entries + field: notes + type: text + meta: + collection: entries + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: input-multiline + note: Free-form notes + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: entries + data_type: text + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entries + field: date_created + type: timestamp + meta: + collection: entries + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 9 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: entries + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entries + field: date_updated + type: timestamp + meta: + collection: entries + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 10 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: entries + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_crew + field: id + type: uuid + meta: + collection: entry_crew + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: + - uuid + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: entry_crew + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_crew + field: entry_id + type: uuid + meta: + collection: entry_crew + conditions: null + display: null + display_options: null + field: entry_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: entry_id + table: entry_crew + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: entries + foreign_key_column: id + - collection: entry_crew + field: user_id + type: uuid + meta: + collection: entry_crew + conditions: null + display: null + display_options: null + field: user_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{first_name}} {{last_name}} ({{email}})' + readonly: false + required: true + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_id + table: entry_crew + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: entry_crew + field: role + type: string + meta: + collection: entry_crew + conditions: null + display: null + display_options: null + field: role + group: null + hidden: false + interface: select-dropdown + note: >- + On-track racing role (distinct from organization_users.role which is a + permissions concept) + options: + choices: + - text: Pilot + value: pilot + - text: Co-Pilot + value: co-pilot + - text: Navigator + value: navigator + - text: Mechanic + value: mechanic + - text: Rider + value: rider + - text: Runner + value: runner + - text: Hiker + value: hiker + readonly: false + required: true + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: role + table: entry_crew + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_crew + field: date_created + type: timestamp + meta: + collection: entry_crew + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: entry_crew + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_crew + field: date_updated + type: timestamp + meta: + collection: entry_crew + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: entry_crew + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_devices + field: id + type: uuid + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: + - uuid + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: entry_devices + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_devices + field: entry_id + type: uuid + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: entry_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: entry_id + table: entry_devices + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: entries + foreign_key_column: id + - collection: entry_devices + field: device_id + type: uuid + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: device_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{model}} {{imei}}' + readonly: false + required: true + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: device_id + table: entry_devices + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: devices + foreign_key_column: id + - collection: entry_devices + field: assigned_user_id + type: uuid + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: assigned_user_id + group: null + hidden: false + interface: select-dropdown-m2o + note: >- + Nullable: null = vehicle-mounted (hardwired or backup); set = body-worn + on this crew member (panic button) + options: + template: '{{first_name}} {{last_name}}' + readonly: false + required: false + searchable: true + sort: 4 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: assigned_user_id + table: entry_devices + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id + - collection: entry_devices + field: mount_position + type: string + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: mount_position + group: null + hidden: false + interface: input + note: >- + Free-form mount label (e.g. "panic_button_pilot", "hardwired_dash", + "backup_chassis") + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: mount_position + table: entry_devices + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_devices + field: date_created + type: timestamp + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 6 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: entry_devices + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: entry_devices + field: date_updated + type: timestamp + meta: + collection: entry_devices + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 7 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: entry_devices + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: id + type: uuid + meta: + collection: events + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + searchable: true + sort: 1 + special: + - uuid + translations: null + validation: null + validation_message: null + width: full + schema: + name: id + table: events + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + is_indexed: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: organization_id + type: uuid + meta: + collection: events + conditions: null + display: null + display_options: null + field: organization_id + group: null + hidden: false + interface: select-dropdown-m2o + note: Owning org. An event belongs to exactly one org. + options: + template: '{{name}}' + readonly: false + required: true + searchable: true + sort: 2 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: organization_id + table: events + data_type: uuid + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: organizations + foreign_key_column: id + - collection: events + field: name + type: string + meta: + collection: events + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: Display name (e.g. "Rally Albania 2026") + options: null + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: name + table: events + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: slug + type: string + meta: + collection: events + conditions: null + display: null + display_options: null + field: slug + group: null + hidden: false + interface: input + note: >- + URL-friendly identifier, unique within the org (composite unique + constraint enforced via db-init/005) + options: + slug: true + trim: true + readonly: false + required: true + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: slug + table: events + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: discipline + type: string + meta: + collection: events + conditions: null + display: null + display_options: null + field: discipline + group: null + hidden: false + interface: select-dropdown + note: 'Drives validation: rally requires vehicle, trail-run/hike does not, etc.' + options: + choices: + - text: Rally + value: rally + - text: Time Trial + value: time-trial + - text: Regatta + value: regatta + - text: Trail Run + value: trail-run + - text: Hike + value: hike + readonly: false + required: true + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: discipline + table: events + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: starts_at + type: timestamp + meta: + collection: events + conditions: null + display: null + display_options: null + field: starts_at + group: null + hidden: false + interface: datetime + note: Event window begin + options: null + readonly: false + required: true + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: starts_at + table: events + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: ends_at + type: timestamp + meta: + collection: events + conditions: null + display: null + display_options: null + field: ends_at + group: null + hidden: false + interface: datetime + note: Event window end + options: null + readonly: false + required: true + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: ends_at + table: events + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: regulation_doc_url + type: string + meta: + collection: events + conditions: null + display: null + display_options: null + field: regulation_doc_url + group: null + hidden: false + interface: input + note: External URL to the rulebook PDF/page + options: null + readonly: false + required: false + searchable: true + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: regulation_doc_url + table: events + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: notes + type: text + meta: + collection: events + conditions: null + display: null + display_options: null + field: notes + group: null + hidden: false + interface: input-multiline + note: Free-form notes + options: null + readonly: false + required: false + searchable: true + sort: 9 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: events + data_type: text + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: date_created + type: timestamp + meta: + collection: events + conditions: null + display: null + display_options: null + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 10 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: events + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: events + field: date_updated + type: timestamp + meta: + collection: events + conditions: null + display: null + display_options: null + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 11 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: events + data_type: timestamp with time zone + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_indexed: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null - collection: organization_devices field: id type: uuid @@ -2031,6 +3933,216 @@ systemFields: schema: is_indexed: true relations: + - collection: classes + field: event_id + related_collection: events + meta: + junction_field: null + many_collection: classes + many_field: event_id + one_allowed_collections: null + one_collection: events + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: classes + column: event_id + foreign_key_table: events + foreign_key_column: id + constraint_name: classes_event_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entries + field: event_id + related_collection: events + meta: + junction_field: null + many_collection: entries + many_field: event_id + one_allowed_collections: null + one_collection: events + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entries + column: event_id + foreign_key_table: events + foreign_key_column: id + constraint_name: entries_event_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entries + field: vehicle_id + related_collection: vehicles + meta: + junction_field: null + many_collection: entries + many_field: vehicle_id + one_allowed_collections: null + one_collection: vehicles + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entries + column: vehicle_id + foreign_key_table: vehicles + foreign_key_column: id + constraint_name: entries_vehicle_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entries + field: class_id + related_collection: classes + meta: + junction_field: null + many_collection: entries + many_field: class_id + one_allowed_collections: null + one_collection: classes + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entries + column: class_id + foreign_key_table: classes + foreign_key_column: id + constraint_name: entries_class_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entry_crew + field: entry_id + related_collection: entries + meta: + junction_field: null + many_collection: entry_crew + many_field: entry_id + one_allowed_collections: null + one_collection: entries + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entry_crew + column: entry_id + foreign_key_table: entries + foreign_key_column: id + constraint_name: entry_crew_entry_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entry_crew + field: user_id + related_collection: directus_users + meta: + junction_field: null + many_collection: entry_crew + many_field: user_id + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entry_crew + column: user_id + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: entry_crew_user_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entry_devices + field: entry_id + related_collection: entries + meta: + junction_field: null + many_collection: entry_devices + many_field: entry_id + one_allowed_collections: null + one_collection: entries + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entry_devices + column: entry_id + foreign_key_table: entries + foreign_key_column: id + constraint_name: entry_devices_entry_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entry_devices + field: device_id + related_collection: devices + meta: + junction_field: null + many_collection: entry_devices + many_field: device_id + one_allowed_collections: null + one_collection: devices + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entry_devices + column: device_id + foreign_key_table: devices + foreign_key_column: id + constraint_name: entry_devices_device_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: entry_devices + field: assigned_user_id + related_collection: directus_users + meta: + junction_field: null + many_collection: entry_devices + many_field: assigned_user_id + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: entry_devices + column: assigned_user_id + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: entry_devices_assigned_user_id_foreign + on_update: NO ACTION + on_delete: SET NULL + - collection: events + field: organization_id + related_collection: organizations + meta: + junction_field: null + many_collection: events + many_field: organization_id + one_allowed_collections: null + one_collection: organizations + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: events + column: organization_id + foreign_key_table: organizations + foreign_key_column: id + constraint_name: events_organization_id_foreign + on_update: NO ACTION + on_delete: RESTRICT - collection: organization_devices field: organization_id related_collection: organizations