docs(form-builder): WS-5c sign-off — SCHEMA v2.5 + ARCH v1.7 §8 + addendum Q3
SCHEMA v2.5: - form_fields: conditional_logic row removed; cross-reference note added pointing at the two new tables and the addendum Q3 WS-5c Uitvoering (no library mirror). - New sections: form_field_conditional_logic_groups (tree nodes, adjacency-list via parent_group_id) and form_field_conditional_logic_conditions (leaves; value JSON nullable for empty/not_empty). Both tables use the Q2 declarative FK-chain resolver via tenantScopeStrategy() — group chain 3 hops, condition chain 4 hops (fits the WS-5c-raised cap of 5). ARCH v1.7 §8 restructured into sub-sections mirroring the §17.4 / §17.5 pattern: - 8.1 Tree structure (read-side contract) - 8.2 Relational tables (column specs, cascade, scope) - 8.3 Service boundary (logicFor/replaceLogic/toJsonShape/ assertSpecsValid/assertNoCycles) - 8.4 Operator catalogues (group + comparison) - 8.5 Cycle detection (contract preserved, implementation moved) - 8.6 Activity log (dual-events: field.updated + field.conditional_logic_replaced; FormField subject only) - 8.7 Legacy JSON migration (strict dispatch, rollback reversible) Addendum Q3 extended with "Uitvoering — WS-5c (2026-04-26)": - No-library-mirror decision reaffirmed (simple FK, no morph) - Two-table tree-structure rationale (groups + conditions semantic purity over single-table mixed-nullables) - OrganisationScope cap raise 3 → 5, rationale: legitimate 4-hop conditions chain + headroom for future deeper trees without denormalising form_field_id onto conditions - Cycle detection migrated to service, contract unchanged - Snapshot + resource JSON contract byte-identical via toJsonShape - Strict validator on save at FormRequest boundary - Scope-sibling discipline: WS-5c adds two FK-chain models (not morph); base-class extraction still parked for WS-5d Sign-off table: WS-5c afronding 2026-04-26 added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
# Crewli — Core Database Schema
|
||||
|
||||
> Source: Design Document v1.3 — Section 3.5
|
||||
> **Version: 2.4** — Updated April 2026
|
||||
> **Version: 2.5** — Updated April 2026
|
||||
>
|
||||
> **Changelog:**
|
||||
>
|
||||
> - v2.5: WS-5c — `form_fields.conditional_logic` JSON column **dropped**;
|
||||
> replaced by a two-table relational tree:
|
||||
> `form_field_conditional_logic_groups` (AND/OR nodes with optional
|
||||
> parent_group_id for nesting) + `form_field_conditional_logic_conditions`
|
||||
> (leaves: field_slug + comparison_operator + value JSON). Simple FK to
|
||||
> `form_fields` — per addendum Q3 the library is explicitly **out of
|
||||
> scope** for conditional_logic (no library mirror, no polymorphic morph
|
||||
> on these tables). Snapshot + API resource JSON shape preserved
|
||||
> byte-for-byte via `FormFieldConditionalLogicService::toJsonShape`.
|
||||
> `OrganisationScope` FK-chain cap raised from 3 to 5 hops to
|
||||
> accommodate the 4-hop conditions chain (condition → group → field →
|
||||
> schema → organisation_id column) without denormalising `form_field_id`
|
||||
> onto conditions. See ARCH-FORM-BUILDER.md §8 and
|
||||
> ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 §Q3 WS-5c Uitvoering.
|
||||
> - v2.4: WS-5b completion — `form_field_configs` relational table lands
|
||||
> alongside `form_field_validation_rules` (both from WS-5b). Holds
|
||||
> non-validation per-field configuration (`tag_categories`,
|
||||
@@ -2052,7 +2066,6 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| `is_unique` | bool | default: false — uniqueness enforced in `FormValueService` (ARCH §4.2.1) |
|
||||
| `is_pii` | bool | default: false — drives retention + anonymisation |
|
||||
| `display_width` | string(10) | default: `full`; `FormFieldDisplayWidth` enum |
|
||||
| `conditional_logic` | JSON nullable | show_when rules; cycle detection on save |
|
||||
| `role_restrictions` | JSON nullable | Per-field RBAC driving `FieldAccessService` |
|
||||
| `translations` | JSON nullable | `{ <locale>: { label, help_text, options } }` |
|
||||
| `value_storage_hint` | string(10) | default: `json`. `FormValueStorageHint` enum — guides typed-column population |
|
||||
@@ -2061,7 +2074,7 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete preserves history |
|
||||
|
||||
**Relations:** `belongsTo` schema, section (nullable), libraryField; `hasMany` form_values; `morphMany` form_field_bindings as `owner`; `morphMany` form_field_validation_rules as `owner`; `morphMany` form_field_configs as `owner`
|
||||
**Relations:** `belongsTo` schema, section (nullable), libraryField; `hasMany` form_values, conditionalLogicGroups; `morphMany` form_field_bindings as `owner`; `morphMany` form_field_validation_rules as `owner`; `morphMany` form_field_configs as `owner`
|
||||
**Indexes:** `(form_schema_id, sort_order)`, `(form_schema_id, is_filterable)`, `(library_field_id)`, `(form_schema_id, slug)`
|
||||
**Soft delete:** yes
|
||||
|
||||
@@ -2073,6 +2086,13 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
> table (ARCH-FORM-BUILDER.md §17.4). The column was dropped in WS-5b;
|
||||
> see `/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md` §Q3 WS-5b
|
||||
> Uitvoering for the full catalogue and migration notes.
|
||||
>
|
||||
> Conditional logic moved to the relational
|
||||
> `form_field_conditional_logic_groups` + `form_field_conditional_logic_conditions`
|
||||
> tables (ARCH-FORM-BUILDER.md §8). The column was dropped in WS-5c; see
|
||||
> `/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md` §Q3 WS-5c
|
||||
> Uitvoering. No library mirror — addendum Q3 excludes library from
|
||||
> conditional_logic scope.
|
||||
|
||||
---
|
||||
|
||||
@@ -2166,6 +2186,55 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
|
||||
---
|
||||
|
||||
### `form_field_conditional_logic_groups`
|
||||
|
||||
> Tree nodes (root + branches) of the relational conditional-logic
|
||||
> structure that replaced `form_fields.conditional_logic` JSON in WS-5c.
|
||||
> Per addendum Q3, only FormField is in scope — no polymorphic morph,
|
||||
> just a simple FK to `form_fields`. Nesting is adjacency-list via
|
||||
> `parent_group_id`; leaves live in `form_field_conditional_logic_conditions`.
|
||||
> ARCH-FORM-BUILDER.md §8.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------ | --------------------- | ------------------------------------------------------------------------------ |
|
||||
| `id` | ULID | PK |
|
||||
| `form_field_id` | ULID FK | → form_fields, cascade delete |
|
||||
| `parent_group_id` | ULID FK nullable | → form_field_conditional_logic_groups, cascade delete; null at the tree root |
|
||||
| `operator` | string(10) | `FormFieldConditionalLogicGroupOperator` enum: `all` (AND) \| `any` (OR) |
|
||||
| `sort_order` | int unsigned | default: 0; ordering within parent_group_id |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Relations:** `belongsTo` form_field, parentGroup; `hasMany` childGroups, conditions
|
||||
**Indexes:** `(form_field_id)`, `(parent_group_id, sort_order)`
|
||||
**Global scope:** `OrganisationScope` via `tenantScopeStrategy()` — FK-chain `group → field → schema → organisation_id` (3 hops, inside the cap). Escape hatch: `withoutGlobalScope(OrganisationScope::class)`.
|
||||
**Soft delete:** no — conditional logic is current state, not audit
|
||||
|
||||
---
|
||||
|
||||
### `form_field_conditional_logic_conditions`
|
||||
|
||||
> Leaves of the relational conditional-logic tree. Each row holds one
|
||||
> `(field_slug, comparison_operator, value)` comparison attached to a
|
||||
> parent group. `value` is JSON nullable — scalar for most operators,
|
||||
> array for `in`/`not_in`, null for `empty`/`not_empty`. ARCH-FORM-BUILDER.md §8.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ---------------------- | --------------------- | ------------------------------------------------------------------------------ |
|
||||
| `id` | ULID | PK |
|
||||
| `group_id` | ULID FK | → form_field_conditional_logic_groups, cascade delete |
|
||||
| `field_slug` | string(100) | references `form_fields.slug` within the same schema (not a FK — the service layer resolves it and raises on unknown slugs) |
|
||||
| `comparison_operator` | string(20) | `FormFieldConditionalLogicConditionOperator` enum: equals, not_equals, contains, not_contains, in, not_in, greater_than, less_than, empty, not_empty |
|
||||
| `value` | JSON nullable | per-operator; null for `empty`/`not_empty` |
|
||||
| `sort_order` | int unsigned | default: 0; ordering within the parent group alongside sub-groups |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Relations:** `belongsTo` group
|
||||
**Indexes:** `(group_id, sort_order)`, `(field_slug)`
|
||||
**Global scope:** `OrganisationScope` via `tenantScopeStrategy()` — FK-chain `condition → group → field → schema → organisation_id` (4 hops; fits within the WS-5c-raised cap of 5). Escape hatch: `withoutGlobalScope(OrganisationScope::class)`.
|
||||
**Soft delete:** no — conditional logic is current state, not audit
|
||||
|
||||
---
|
||||
|
||||
### `form_submissions`
|
||||
|
||||
> One submission per `(schema, subject)` in `single` / `draft_single`
|
||||
|
||||
Reference in New Issue
Block a user