From 6f376a479fd4bf5618fe7131c28ab460a0cddb17 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 2 May 2026 09:39:04 +0200 Subject: [PATCH] =?UTF-8?q?Task=201.4=20=E2=80=94=20Org-level=20catalog=20?= =?UTF-8?q?collections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven collections + 3 directus_users custom fields, captured as snapshots/schema.yaml (53 KB, 2,159 lines). Collections: - organizations — UUID PK, name, slug UNIQUE - vehicles — UUID PK, make/model required, year/cc/vin/plate optional - devices — UUID PK, imei UNIQUE, model required - organization_users — junction with role enum (org-admin, race-director, marshal, timekeeper, participant, viewer) - organization_vehicles — junction with registered_at - organization_devices — junction with registered_at - directus_users — extended with phone, birth_date, nationality Six M2O relations on the junctions, all ON DELETE RESTRICT (matching the schema-draft decision: deletion of an org/vehicle/device/user requires explicit cleanup of dependents). db-init/004_junction_unique_constraints.sql adds the composite UNIQUE constraints on the three junctions: organization_users (organization_id, user_id) organization_vehicles (organization_id, vehicle_id) organization_devices (organization_id, device_id) Composite uniqueness lives in db-init rather than the Directus snapshot because Directus's snapshot YAML format only captures single-column unique constraints (the field-level is_unique flag). The migration file documents the split inline. Driven via the directus-local MCP server rather than admin-UI clicking — programmatic create-collection/create-field/create-relation calls against the running Directus instance, then `pnpm run schema:snapshot` to capture the canonical YAML. Live-verified: db-init/004 applies cleanly on container restart (0 rows in the empty junctions, no constraint violations); schema-apply against a snapshot-empty boot still skips correctly; all seven new collections show up in the admin UI's data model navigation. Snapshot includes positions and migrations_applied as auto-discovered ghost entries (Directus introspects all public-schema tables). Harmless — db-init creates them before schema-apply runs, so snapshot apply just finds them already present. ROADMAP marks 1.4 done. Phase 1 progress: 6/9 tasks complete (1.1, 1.2, 1.3, 1.4, 1.6, 1.7); 1.5, 1.8, 1.9 remain. --- .planning/ROADMAP.md | 4 +- .../04-org-catalog-collections.md | 40 +- db-init/004_junction_unique_constraints.sql | 60 + snapshots/schema.yaml | 2159 +++++++++++++++++ 4 files changed, 2260 insertions(+), 3 deletions(-) create mode 100644 db-init/004_junction_unique_constraints.sql create mode 100644 snapshots/schema.yaml diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b09b440..7dfed58 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.6, 1.7 done; 1.4, 1.5, 1.8, 1.9 remaining) +**Status:** 🟨 In progress (1.1, 1.2, 1.3, 1.4, 1.6, 1.7 done; 1.5, 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) @@ -52,7 +52,7 @@ These rules govern every task. Any deviation must be discussed and documented as | 1.1 | [Project scaffold](./phase-1-slice-1-schema/01-project-scaffold.md) | 🟩 | pending user commit | | 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) | ⬜ | — | +| 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.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 | diff --git a/.planning/phase-1-slice-1-schema/04-org-catalog-collections.md b/.planning/phase-1-slice-1-schema/04-org-catalog-collections.md index 9330bb9..ed189c8 100644 --- a/.planning/phase-1-slice-1-schema/04-org-catalog-collections.md +++ b/.planning/phase-1-slice-1-schema/04-org-catalog-collections.md @@ -126,4 +126,42 @@ Unique constraint: `(organization_id, device_id)`. ## Done -(Fill in commit SHA + one-line note when this lands.) +**Implementation landed and live-verified 2026-05-02.** All 7 collections live in Directus, snapshot captured at 53,450 bytes / 2,159 lines. + +**Driven via the `directus-local` MCP server** rather than the admin UI — same canonical result (`directus_collections` / `directus_fields` / `directus_relations` rows + actual Postgres tables), captured cleanly by `directus schema snapshot`. This was the API-driven path the spec hinted at; sub-agents can't inherit MCP from the parent conversation, so this work was driven directly without delegation. + +**Created:** +- `organizations` — 5 fields (id UUID PK, name, slug unique, date_created, date_updated). +- `vehicles` — 10 fields (id UUID PK, make, model, year, engine_cc, vin, plate_number, notes, date_created, date_updated). No ownership fields. +- `devices` — 7 fields (id UUID PK, imei UNIQUE, model, serial_number, notes, date_created, date_updated). +- `directus_users` — 3 custom fields added (phone, birth_date, nationality). +- `organization_users` — 7 fields (id UUID PK, organization_id M2O, user_id M2O, role enum dropdown with 6 values, joined_at, date_created, date_updated). +- `organization_vehicles` — 6 fields (id UUID PK, organization_id M2O, vehicle_id M2O, registered_at, date_created, date_updated). +- `organization_devices` — 6 fields (id UUID PK, organization_id M2O, device_id M2O, registered_at, date_created, date_updated). +- 6 M2O relations on the junctions, all with `ON DELETE RESTRICT`. + +**Composite unique constraints landed via `db-init/004_junction_unique_constraints.sql`** because Directus's snapshot YAML format does not capture composite unique constraints (only single-column ones via `is_unique`). The migration adds: +- `organization_users (organization_id, user_id)` +- `organization_vehicles (organization_id, vehicle_id)` +- `organization_devices (organization_id, device_id)` + +Boot logs confirm: `[db-init] apply 004_junction_unique_constraints.sql` → `[db-init] done 004_junction_unique_constraints.sql` → assertion block passes. + +**Snapshot review (`snapshots/schema.yaml`):** +- 8 collections registered (the 7 above + `positions` and `migrations_applied` as ghost entries — Directus auto-discovers tables in the public schema and registers minimal metadata for them, even though they're owned by db-init/processor not Directus). The ghost entries are harmless: schema apply against a fresh DB sees them already created by db-init and skips DDL. +- `directus_users` custom fields round-trip correctly (no need for the spec's fallback `user_profiles` workaround). +- All 6 M2O relations present in the relations section. +- File size 53,450 bytes — well under the 200KB sanity threshold. + +**Acceptance criteria status:** +- ✅ All seven collections exist with the fields specified. +- ✅ Required fields flagged (organizations.name/slug, devices.imei/model, vehicles.make/model, junction org/target/role). +- ✅ Single-column unique constraints (organizations.slug, devices.imei) enforced. +- ✅ Composite unique constraints on junctions enforced via db-init/004 (assertion block confirms). +- ✅ M2O relations clickable in admin UI (Directus auto-resolves the dropdowns from the relation metadata). +- ✅ No permission policies attached — admin-only by default. +- ✅ `pnpm run schema:snapshot` produces snapshots/schema.yaml with all 7 collections present. +- ⏳ End-to-end test (manually create org → user → org_user via admin UI) — pending user. +- ⏳ Apply-to-fresh-DB roundtrip — pending CI dry-run in task 1.8. + +**Phase 5 follow-up note (not blocking):** boot logs still WARN about `positions` lacking a PK. Already documented in task 1.7's Done section. diff --git a/db-init/004_junction_unique_constraints.sql b/db-init/004_junction_unique_constraints.sql new file mode 100644 index 0000000..aa06db5 --- /dev/null +++ b/db-init/004_junction_unique_constraints.sql @@ -0,0 +1,60 @@ +-- 004_junction_unique_constraints.sql +-- Composite UNIQUE constraints on the three org-junction tables. +-- +-- Why this lives in db-init/ rather than being captured by Directus snapshot: +-- Directus's field-level `is_unique` flag only generates single-column +-- unique constraints. Junction tables need composite uniqueness on the +-- pair (org, target) so the same user/vehicle/device cannot be registered +-- twice within the same org. The snapshot YAML format does NOT capture +-- composite unique constraints, so Directus cannot round-trip them. +-- They belong here, in the same place the positions hypertable's DDL lives. +-- +-- Owned by: task 1.4 (org catalog collections). The constraints are part of +-- the data model contract, not a separate Phase 1 migration concern. +-- +-- Idempotency: ALTER TABLE ... ADD CONSTRAINT is NOT idempotent. The +-- migrations_applied guard table ensures this file runs at most once per +-- environment. If a constraint already exists (e.g. ad-hoc on an existing +-- stage DB), the operator must INSERT INTO migrations_applied (filename, +-- checksum) VALUES ('004_junction_unique_constraints.sql', '') to +-- skip this file on next boot. + +ALTER TABLE organization_users + ADD CONSTRAINT organization_users_org_user_unique + UNIQUE (organization_id, user_id); + +ALTER TABLE organization_vehicles + ADD CONSTRAINT organization_vehicles_org_vehicle_unique + UNIQUE (organization_id, vehicle_id); + +ALTER TABLE organization_devices + ADD CONSTRAINT organization_devices_org_device_unique + UNIQUE (organization_id, device_id); + +-- ------------------------------------------------------------------------- +-- Assertion block: verify all three constraints landed. +-- ------------------------------------------------------------------------- +DO $$ BEGIN + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'organization_users_org_user_unique' + ) THEN + RAISE EXCEPTION 'organization_users composite unique constraint missing'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'organization_vehicles_org_vehicle_unique' + ) THEN + RAISE EXCEPTION 'organization_vehicles composite unique constraint missing'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'organization_devices_org_device_unique' + ) THEN + RAISE EXCEPTION 'organization_devices composite unique constraint missing'; + END IF; + +END $$; diff --git a/snapshots/schema.yaml b/snapshots/schema.yaml new file mode 100644 index 0000000..a61e3bd --- /dev/null +++ b/snapshots/schema.yaml @@ -0,0 +1,2159 @@ +version: 1 +directus: 11.17.4 +vendor: postgres +collections: + - collection: devices + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: devices + color: null + display_template: '{{model}} — {{imei}}' + group: null + hidden: false + icon: gps_fixed + item_duplication_fields: null + note: >- + Durable GPS tracker catalog. IMEI is the canonical identifier. Belongs + to orgs via organization_devices. + preview_url: null + singleton: false + sort: 3 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: devices + - collection: migrations_applied + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: migrations_applied + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: migrations_applied + - collection: organization_devices + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: organization_devices + color: null + display_template: '{{device_id.model}} {{device_id.imei}} — {{organization_id.name}}' + group: null + hidden: false + icon: device_hub + item_duplication_fields: null + note: 'Junction: which devices belong to which orgs.' + preview_url: null + singleton: false + sort: 6 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: organization_devices + - collection: organization_users + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: organization_users + color: null + display_template: >- + {{user_id.first_name}} {{user_id.last_name}} — {{role}} — + {{organization_id.name}} + group: null + hidden: false + icon: person_add + item_duplication_fields: null + note: 'Junction: which users belong to which orgs and with what role.' + preview_url: null + singleton: false + sort: 4 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: organization_users + - collection: organization_vehicles + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: organization_vehicles + color: null + display_template: '{{vehicle_id.make}} {{vehicle_id.model}} — {{organization_id.name}}' + group: null + hidden: false + icon: garage + item_duplication_fields: null + note: 'Junction: which vehicles belong to which orgs.' + preview_url: null + singleton: false + sort: 5 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: organization_vehicles + - collection: organizations + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: organizations + color: null + display_template: '{{name}}' + group: null + hidden: false + icon: corporate_fare + item_duplication_fields: null + note: >- + Tenant root. Each event, vehicle, device and user belongs to one or more + organizations via junction tables. + preview_url: null + singleton: false + sort: 1 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: organizations + - collection: positions + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: positions + color: null + display_template: null + group: null + hidden: false + icon: null + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: positions + - collection: vehicles + meta: + accountability: all + archive_app_filter: true + archive_field: null + archive_value: null + collapse: open + collection: vehicles + color: null + display_template: '{{make}} {{model}} ({{year}})' + group: null + hidden: false + icon: directions_car + item_duplication_fields: null + note: >- + Durable vehicle catalog. Belongs to orgs via organization_vehicles. No + ownership modelling — vehicles are org-scoped only. + preview_url: null + singleton: false + sort: 2 + sort_field: null + translations: null + unarchive_value: null + versioning: false + schema: + name: vehicles +fields: + - collection: devices + field: id + type: uuid + meta: + collection: 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: 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: devices + field: imei + type: string + meta: + collection: devices + conditions: null + display: null + display_options: null + field: imei + group: null + hidden: false + interface: input + note: Canonical 15-digit IMEI. UNIQUE across the whole system. + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: imei + table: devices + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + 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: model + type: string + meta: + collection: devices + conditions: null + display: null + display_options: null + field: model + group: null + hidden: false + interface: input + note: >- + Hardware model (e.g. "FMB920", "FMB003") — drives IO mapping in the + processor service. + options: null + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: model + table: devices + 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: devices + field: serial_number + type: string + meta: + collection: devices + conditions: null + display: null + display_options: null + field: serial_number + group: null + hidden: false + interface: input + note: Manufacturer serial number (optional) + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: serial_number + table: 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: devices + field: notes + type: text + meta: + collection: devices + 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: 5 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: notes + table: devices + 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: devices + field: date_created + type: timestamp + meta: + collection: 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: 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: devices + field: date_updated + type: timestamp + meta: + collection: 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: 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: directus_users + field: phone + type: string + meta: + collection: directus_users + conditions: null + display: null + display_options: null + field: phone + group: null + hidden: false + interface: input + note: Phone number (optional) + options: null + readonly: false + required: false + searchable: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: phone + table: directus_users + 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: directus_users + field: birth_date + type: date + meta: + collection: directus_users + conditions: null + display: null + display_options: null + field: birth_date + group: null + hidden: false + interface: datetime + note: >- + Used for age-derived class eligibility (e.g. M-5/M-6/M-7 senior/veteran + classes) + options: null + readonly: false + required: false + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: birth_date + table: directus_users + data_type: date + 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: directus_users + field: nationality + type: string + meta: + collection: directus_users + conditions: null + display: null + display_options: null + field: nationality + group: null + hidden: false + interface: input + note: ISO 3166-1 alpha-2 country code (e.g. "AL", "IT") + options: + placeholder: AL + readonly: false + required: false + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: nationality + table: directus_users + 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: organization_devices + field: id + type: uuid + meta: + collection: organization_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: organization_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: organization_devices + field: organization_id + type: uuid + meta: + collection: organization_devices + conditions: null + display: null + display_options: null + field: organization_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + 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: organization_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: organizations + foreign_key_column: id + - collection: organization_devices + field: device_id + type: uuid + meta: + collection: organization_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: organization_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: organization_devices + field: registered_at + type: timestamp + meta: + collection: organization_devices + conditions: null + display: null + display_options: null + field: registered_at + group: null + hidden: false + interface: datetime + note: When the device was registered with this org + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: registered_at + table: organization_devices + 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: organization_devices + field: date_created + type: timestamp + meta: + collection: organization_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: 5 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: organization_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: organization_devices + field: date_updated + type: timestamp + meta: + collection: organization_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: 6 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: organization_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: organization_users + field: id + type: uuid + meta: + collection: organization_users + 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: organization_users + 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: organization_users + field: organization_id + type: uuid + meta: + collection: organization_users + conditions: null + display: null + display_options: null + field: organization_id + group: null + hidden: false + interface: select-dropdown-m2o + note: Tenant 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: organization_users + 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: organization_users + field: user_id + type: uuid + meta: + collection: organization_users + conditions: null + display: null + display_options: null + field: user_id + group: null + hidden: false + interface: select-dropdown-m2o + note: Directus user + 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: organization_users + 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: organization_users + field: role + type: string + meta: + collection: organization_users + conditions: null + display: null + display_options: null + field: role + group: null + hidden: false + interface: select-dropdown + note: >- + Role for this user in this org. Drives Directus permission policies in + Phase 4. + options: + choices: + - text: Org Admin + value: org-admin + - text: Race Director + value: race-director + - text: Marshal + value: marshal + - text: Timekeeper + value: timekeeper + - text: Participant + value: participant + - text: Viewer + value: viewer + readonly: false + required: true + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: role + table: organization_users + 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: organization_users + field: joined_at + type: timestamp + meta: + collection: organization_users + conditions: null + display: null + display_options: null + field: joined_at + group: null + hidden: false + interface: datetime + note: When the user joined this org + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: joined_at + table: organization_users + 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: organization_users + field: date_created + type: timestamp + meta: + collection: organization_users + 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: organization_users + 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_users + field: date_updated + type: timestamp + meta: + collection: organization_users + 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: organization_users + 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_vehicles + field: id + type: uuid + meta: + collection: organization_vehicles + 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: organization_vehicles + 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: organization_vehicles + field: organization_id + type: uuid + meta: + collection: organization_vehicles + conditions: null + display: null + display_options: null + field: organization_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + 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: organization_vehicles + 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: organization_vehicles + field: vehicle_id + type: uuid + meta: + collection: organization_vehicles + conditions: null + display: null + display_options: null + field: vehicle_id + group: null + hidden: false + interface: select-dropdown-m2o + note: null + options: + template: '{{make}} {{model}}' + readonly: false + required: true + searchable: true + sort: 3 + special: + - m2o + translations: null + validation: null + validation_message: null + width: half + schema: + name: vehicle_id + table: organization_vehicles + 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: vehicles + foreign_key_column: id + - collection: organization_vehicles + field: registered_at + type: timestamp + meta: + collection: organization_vehicles + conditions: null + display: null + display_options: null + field: registered_at + group: null + hidden: false + interface: datetime + note: When the vehicle was registered with this org + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: registered_at + table: organization_vehicles + 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: organization_vehicles + field: date_created + type: timestamp + meta: + collection: organization_vehicles + 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: organization_vehicles + 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_vehicles + field: date_updated + type: timestamp + meta: + collection: organization_vehicles + 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: organization_vehicles + 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: organizations + field: id + type: uuid + meta: + collection: organizations + 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: organizations + 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: organizations + field: name + type: string + meta: + collection: organizations + conditions: null + display: null + display_options: null + field: name + group: null + hidden: false + interface: input + note: Display name (e.g. "Motorsport Club Albania") + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: name + table: organizations + 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: organizations + field: slug + type: string + meta: + collection: organizations + conditions: null + display: null + display_options: null + field: slug + group: null + hidden: false + interface: input + note: URL-friendly identifier, unique across all orgs + options: + slug: true + trim: true + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: slug + table: organizations + data_type: character varying + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + 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: organizations + field: date_created + type: timestamp + meta: + collection: organizations + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 4 + special: + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: organizations + 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: organizations + field: date_updated + type: timestamp + meta: + collection: organizations + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + searchable: true + sort: 5 + special: + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: organizations + 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: vehicles + field: id + type: uuid + meta: + collection: vehicles + 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: vehicles + 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: vehicles + field: make + type: string + meta: + collection: vehicles + conditions: null + display: null + display_options: null + field: make + group: null + hidden: false + interface: input + note: Manufacturer (e.g. "Toyota") + options: null + readonly: false + required: true + searchable: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: make + table: vehicles + 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: vehicles + field: model + type: string + meta: + collection: vehicles + conditions: null + display: null + display_options: null + field: model + group: null + hidden: false + interface: input + note: Model name (e.g. "Land Cruiser 70") + options: null + readonly: false + required: true + searchable: true + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: model + table: vehicles + 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: vehicles + field: year + type: integer + meta: + collection: vehicles + conditions: null + display: null + display_options: null + field: year + group: null + hidden: false + interface: input + note: Model year + options: null + readonly: false + required: false + searchable: true + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: year + table: vehicles + 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: vehicles + field: engine_cc + type: integer + meta: + collection: vehicles + conditions: null + display: null + display_options: null + field: engine_cc + group: null + hidden: false + interface: input + note: Engine displacement in cc — used for class assignment in rally events + options: null + readonly: false + required: false + searchable: true + sort: 5 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: engine_cc + table: vehicles + 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: vehicles + field: vin + type: string + meta: + collection: vehicles + conditions: null + display: null + display_options: null + field: vin + group: null + hidden: false + interface: input + note: Vehicle Identification Number (optional) + options: null + readonly: false + required: false + searchable: true + sort: 6 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: vin + table: vehicles + 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: vehicles + field: plate_number + type: string + meta: + collection: vehicles + conditions: null + display: null + display_options: null + field: plate_number + group: null + hidden: false + interface: input + note: License plate (optional) + options: null + readonly: false + required: false + searchable: true + sort: 7 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: plate_number + table: vehicles + 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: vehicles + field: notes + type: text + meta: + collection: vehicles + 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: vehicles + 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: vehicles + field: date_created + type: timestamp + meta: + collection: vehicles + 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: vehicles + 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: vehicles + field: date_updated + type: timestamp + meta: + collection: vehicles + 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: vehicles + 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 +systemFields: + - collection: directus_activity + field: timestamp + schema: + is_indexed: true + - collection: directus_revisions + field: activity + schema: + is_indexed: true + - collection: directus_revisions + field: parent + schema: + is_indexed: true +relations: + - collection: organization_devices + field: organization_id + related_collection: organizations + meta: + junction_field: null + many_collection: organization_devices + 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: organization_devices + column: organization_id + foreign_key_table: organizations + foreign_key_column: id + constraint_name: organization_devices_organization_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: organization_devices + field: device_id + related_collection: devices + meta: + junction_field: null + many_collection: organization_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: organization_devices + column: device_id + foreign_key_table: devices + foreign_key_column: id + constraint_name: organization_devices_device_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: organization_users + field: organization_id + related_collection: organizations + meta: + junction_field: null + many_collection: organization_users + 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: organization_users + column: organization_id + foreign_key_table: organizations + foreign_key_column: id + constraint_name: organization_users_organization_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: organization_users + field: user_id + related_collection: directus_users + meta: + junction_field: null + many_collection: organization_users + 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: organization_users + column: user_id + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: organization_users_user_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: organization_vehicles + field: organization_id + related_collection: organizations + meta: + junction_field: null + many_collection: organization_vehicles + 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: organization_vehicles + column: organization_id + foreign_key_table: organizations + foreign_key_column: id + constraint_name: organization_vehicles_organization_id_foreign + on_update: NO ACTION + on_delete: RESTRICT + - collection: organization_vehicles + field: vehicle_id + related_collection: vehicles + meta: + junction_field: null + many_collection: organization_vehicles + 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: organization_vehicles + column: vehicle_id + foreign_key_table: vehicles + foreign_key_column: id + constraint_name: organization_vehicles_vehicle_id_foreign + on_update: NO ACTION + on_delete: RESTRICT