docs(form-builder): WS-5b partial sign-off — SCHEMA v2.3 + ARCH v1.5 §17.4 + addendum Q3
This commit is contained in:
@@ -156,6 +156,26 @@ class FormField extends Model
|
||||
|
||||
**Git-log kanttekening commit 3.** De 1Password signer gaf herhaalde "failed to fill whole buffer" errors op de lange HEREDOC body van de bedoelde commit-message; de uiteindelijke commit landde met alleen de titel (`refactor(form-builder): pre-publish check reads form_field_bindings; drop binding JSON columns`, SHA `61719bf`). De volledige rationale — pre-publish check switch van JSON naar relationele query, kolom-drops op `form_fields.binding` en `form_field_library.default_binding`, factory/resource/form-request cleanup, fixture-rewrites — staat in de WS-5a completion notitie, niet in `git show 61719bf`. Amenden is niet geprobeerd: CLAUDE.md verbiedt signed-commit amenden.
|
||||
|
||||
### Uitvoering — WS-5b (2026-04-25)
|
||||
|
||||
WS-5b splitst `form_fields.validation_rules` en `form_field_library.validation_rules` naar een relationele tabel `form_field_validation_rules`. Rij-shape: `owner_type` / `owner_id` (polymorphic morph, hergebruik van de WS-5a aliassen) + `rule_type` string(40, app-enforced enum `FormFieldValidationRuleType`) + `parameters` JSON per rule-type. Rationale: rule_type is de queryable dimensie ("welke velden hanteren regex?"), waarden zijn heterogeen (int voor min_length, string voor regex, array voor mime_types) waardoor JSON *per rij* passend is, niet voor de hele bag.
|
||||
|
||||
**Seed-scan resultaat (Phase A).** Zes distinct top-level keys geobserveerd in seeders/factories/tests/resource-code-paden: `min`, `max`, `regex`, `unique`, `max_priorities`, `tag_categories`. `required` bestond niet in de wild (kolom `is_required` was al de bron van waarheid). Enum-catalogus uitbreiding beperkt tot de vijftien validatie-cases; non-validatie-keys verplaatsen naar een parallelle tabel.
|
||||
|
||||
**Strict-enterprise scope-uitbreiding.** Tijdens de decision gate is WS-5b uitgebreid met een parallelle tabel `form_field_configs` (zie ARCH §17.5, landed in WS-5b commit 5). Rationale: `tag_categories` en `storage_disk` zijn renderings-/upload-configuratie, geen validatie-regels. Ze opnemen in `form_field_validation_rules` zou de tabel-naam semantisch vervuilen — precies het type drift dat deze sprint opruimt. Eén extra tabel, één extra enum, één extra service, één extra scope; géén scope-creep verder dan dat paar.
|
||||
|
||||
**Key-translaties bij backfill.**
|
||||
- `required`, `unique`: WARN-log + skip. Kolom-duplicaten van `is_required` / `is_unique`. In WS-5b commit 3 is de `FormValueService::$rules['unique']` fallback verwijderd — `is_unique` is nu de enige bron.
|
||||
- `max_priorities` → `rule_type = max_selected`: semantisch gelijk (cap op entries in list-valued veld); twee enum-cases voor één semantiek = rot. Seeder (`FormBuilderDevSeeder`) en test-assertie (`PublicFormSeederTest:190`) aangepast om de canonieke naam te gebruiken. Geen bridging in `toJsonShape`; historische snapshots blijven onaangeraakt.
|
||||
- Ambigu `min` / `max`: field-type dispatch — NUMBER → `min_value`/`max_value`; TEXT/TEXTAREA/EMAIL/PHONE/URL → `min_length`/`max_length`; DATE/DATETIME → `date_min`/`date_max`; anders **FAIL** migratie. Type-inappropriate gebruik van `min`/`max` is seed-data-bug; force correction, absorbeer niet stilletjes.
|
||||
- `tag_categories`, `storage_disk`: skipped door de validation-rules backfill met INFO log; commit 5's configs-backfill pickt ze op in `form_field_configs`.
|
||||
|
||||
**Activity log-conventie.** Per §6.7 WS-5a: `field.validation_rules_replaced` wordt alleen geëmit op FormField-subject, niet op FormFieldLibrary. Library-niveau audits leven elders; bewuste convergentie met de binding-pattern.
|
||||
|
||||
**Scope-sibling.** `FormFieldValidationRuleScope` is een near-duplicaat van `FormFieldBindingScope` (zelfde UNION-over-two-owner-chains shape). Base-class extractie is uitgesteld tot WS-5d (waar het derde sibling `form_field_options` een gedeelde vorm moet onthullen); premature abstractie uit twee is nog steeds premature. Cascade-observer `FormFieldBindingsCascadeObserver` is hernoemd naar `FormFieldChildTablesCascadeObserver` en ruimt nu drie child-tabellen op (bindings, validation-rules, en — vanaf commit 5 — configs).
|
||||
|
||||
**Strict validator op save (commit 3).** De vier FormRequests (`StoreFormFieldRequest`, `UpdateFormFieldRequest`, `StoreFormFieldLibraryRequest`, `UpdateFormFieldLibraryRequest`) accepteren `validation_rules` nu als array-of-spec-objects (`[{rule_type, parameters, error_message_key?}, ...]`). Semantische validatie (enum-case + parameter-shape + callback-registry) loopt via `FormFieldValidationRuleService::assertSpecsValid()` in een `after()` hook, zodat bad specs 422 geven vóór enige write. Controllers schrijven niet langer `validation_rules` naar de JSON-kolom; writes gaan uitsluitend via `replaceRules()`.
|
||||
|
||||
---
|
||||
|
||||
## Q4 — Sanctum `personal_access_tokens`
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
# ARCH — Universal Form Builder (v1.4)
|
||||
# ARCH — Universal Form Builder (v1.5)
|
||||
|
||||
> **Source of truth** for Crewli's universal Form Builder architecture.
|
||||
> Any discrepancy with SCHEMA.md is resolved in favour of this document
|
||||
> during the refactor. SCHEMA.md is updated at the end of the refactor.
|
||||
>
|
||||
> **Status:** Approved — WS-5a landed (relational `form_field_bindings`)
|
||||
> **Version:** 1.4 (§6.3 retitled to "Binding row specification"; new
|
||||
> §6.7 "Relational binding table"; §17.3 pre-publish check in present
|
||||
> tense per WS-5a)
|
||||
> **Previous version:** 1.3 (§10.4 public submission lifecycle —
|
||||
> draft/save/submit split with error envelope and drift detection),
|
||||
> **Status:** Approved — WS-5a landed (relational `form_field_bindings`);
|
||||
> WS-5b validation rules landed (relational `form_field_validation_rules`).
|
||||
> **Version:** 1.5 (§17.4 restructured into relational sub-sections:
|
||||
> catalogue, relational table, callback rules, legacy JSON migration).
|
||||
> **Previous versions:**
|
||||
> 1.4 (§6.3 retitled to "Binding row specification"; new §6.7 "Relational
|
||||
> binding table"; §17.3 pre-publish check in present tense per WS-5a),
|
||||
> 1.3 (§10.4 public submission lifecycle — draft/save/submit split with
|
||||
> error envelope and drift detection),
|
||||
> 1.2.1 April 2026 (§31.10 FORM-02 contract),
|
||||
> 1.2 April 2026 (per-purpose lifecycles, integration contracts, user
|
||||
> guidance principles, documentation coverage, in-app copy catalogue)
|
||||
> guidance principles, documentation coverage, in-app copy catalogue).
|
||||
> **Created:** April 2026
|
||||
> **Owner:** Architecture doc; every session reads this before starting
|
||||
>
|
||||
@@ -2206,16 +2209,150 @@ and diffs them against `requiredBindings`. External contract
|
||||
(identity-match, tag sync, entity creation, etc.).
|
||||
5. Add a lifecycle paragraph under §3.2 and a row to the table above.
|
||||
|
||||
### 17.4 Custom validation callbacks
|
||||
### 17.4 Validation rules
|
||||
|
||||
`form_fields.validation_rules` JSON supports callback references:
|
||||
```json
|
||||
{
|
||||
"callback": "App\\Services\\Validators\\KvkValidator@validate"
|
||||
}
|
||||
Pre-WS-5b, validation rules lived as a flat JSON bag on
|
||||
`form_fields.validation_rules` and `form_field_library.validation_rules`.
|
||||
WS-5b moved them to the relational `form_field_validation_rules` table
|
||||
(one row per rule), in parallel with §6.7 (bindings). This section is the
|
||||
canonical home for the rule catalogue, the relational-table shape, the
|
||||
callback-registry integration, and the legacy-key migration notes.
|
||||
|
||||
#### 17.4.1 Rule-type catalogue
|
||||
|
||||
`FormFieldValidationRuleType` (PHP backed enum) is the canonical list of
|
||||
rule_type values. Each case documents the required `parameters` shape
|
||||
that `FormFieldValidationRuleService::replaceRules()` enforces.
|
||||
|
||||
| `rule_type` | `parameters` shape | Notes |
|
||||
| -------------------- | ------------------------------ | ------------------------------------------------------- |
|
||||
| `min_length` | `{"value": int}` | Minimum character length for string-valued fields |
|
||||
| `max_length` | `{"value": int}` | Maximum character length |
|
||||
| `min_value` | `{"value": number}` | Minimum numeric value for NUMBER fields |
|
||||
| `max_value` | `{"value": number}` | Maximum numeric value |
|
||||
| `regex` | `{"pattern": string, "flags": string?}` | `preg_match` pattern; flags optional |
|
||||
| `email_format` | `{}` | Boolean marker — enforces RFC-5321 email shape |
|
||||
| `url_format` | `{}` | Boolean marker — enforces URL shape |
|
||||
| `phone_e164` | `{}` | Boolean marker — enforces E.164 phone shape |
|
||||
| `allowed_mime_types` | `{"mime_types": [string]}` | Whitelist for FILE_UPLOAD / IMAGE_UPLOAD / SIGNATURE |
|
||||
| `max_file_size` | `{"bytes": int}` | Upper bound for uploads |
|
||||
| `min_selected` | `{"value": int}` | Lower bound for multi-value selections |
|
||||
| `max_selected` | `{"value": int}` | Upper bound; covers the legacy `max_priorities` key |
|
||||
| `date_min` | `{"date": string}` | ISO-8601 date; lower bound for DATE / DATETIME |
|
||||
| `date_max` | `{"date": string}` | Upper bound |
|
||||
| `callback` | `{"key": string}` | Registered callback key, see §17.4.3 |
|
||||
|
||||
**Not in the catalogue (deliberate):**
|
||||
|
||||
- `required` — `form_fields.is_required` column is the single source of
|
||||
truth. Any legacy `validation_rules.required` JSON is WARN-logged and
|
||||
skipped at backfill.
|
||||
- `unique` — `form_fields.is_unique` column is the single source of
|
||||
truth. The pre-WS-5b JSON fallback path in `FormValueService` was
|
||||
stripped in WS-5b commit 3.
|
||||
- `tag_categories`, `storage_disk` — not validation rules, they are
|
||||
field-rendering / upload-storage configuration. WS-5b relocates these
|
||||
to a separate `form_field_configs` table (ARCH §17.5, landed in
|
||||
WS-5b commit 5) rather than polluting the validation-rules catalogue.
|
||||
|
||||
**Rule types are app-enforced, not DB enum.** The column is
|
||||
`string(40)` so the enum can extend in application code without a
|
||||
migration — identical rationale to `form_fields.field_type` (§4.2) and
|
||||
`CustomFieldTypeRegistry`.
|
||||
|
||||
#### 17.4.2 Relational table `form_field_validation_rules`
|
||||
|
||||
**Columns** (SCHEMA.md §3.5.12):
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------- | ----------------- | ---------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `owner_type` | string(40) | morph alias: `form_field` or `form_field_library` |
|
||||
| `owner_id` | ULID | parent row |
|
||||
| `rule_type` | string(40) | enum case value |
|
||||
| `parameters` | JSON | per-rule-type bag |
|
||||
| `error_message_key` | string(100) null | optional i18n key for custom rejection copy |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
- **Unique:** `(owner_type, owner_id, rule_type)` — at most one rule of
|
||||
each type per field.
|
||||
- **Indexes:** `(rule_type)` for "which fields enforce regex?" queries,
|
||||
`(owner_type, owner_id)` for per-owner lookups.
|
||||
- **Morph-map aliases** `form_field` and `form_field_library` are reused
|
||||
from WS-5a (`AppServiceProvider::registerMorphMap`) — no new entries
|
||||
needed.
|
||||
|
||||
**Multi-tenancy (`FormFieldValidationRuleScope`).** Sibling to
|
||||
`FormFieldBindingScope` with identical UNION-over-two-owner-chains
|
||||
shape:
|
||||
|
||||
```
|
||||
owner_id ∈ (
|
||||
SELECT id FROM form_fields
|
||||
WHERE form_schema_id ∈ (SELECT id FROM form_schemas WHERE organisation_id = ?)
|
||||
UNION
|
||||
SELECT id FROM form_field_library
|
||||
WHERE organisation_id = ?
|
||||
)
|
||||
```
|
||||
|
||||
Registered callbacks in `config/form_builder.php`:
|
||||
Organisation context resolution mirrors `OrganisationScope`; the escape
|
||||
hatch is
|
||||
`FormFieldValidationRule::withoutGlobalScope(FormFieldValidationRuleScope::class)`.
|
||||
|
||||
Base-class extraction between the two scope classes is deliberately
|
||||
deferred to WS-5d per addendum Q3 — premature abstraction from two
|
||||
siblings is still premature, and WS-5d's `form_field_options` /
|
||||
WS-5c's `form_field_conditional_logic` may surface a different shared
|
||||
shape.
|
||||
|
||||
**Service boundary (`FormFieldValidationRuleService`).** All writes go
|
||||
through the service — no controller writes rules directly on the
|
||||
model. The service owns:
|
||||
|
||||
- `rulesFor(owner)` — eager, scope-aware fetch.
|
||||
- `replaceRules(owner, specs)` — transactional delete + insert;
|
||||
validates every spec's `rule_type` against the enum and `parameters`
|
||||
against the per-rule shape (including callback-key registry check).
|
||||
Logs `field.validation_rules_replaced` on the owning FormField
|
||||
subject (matching the WS-5a convention: library-level changes are
|
||||
silent in activity log).
|
||||
- `copyRules(library, field)` — row-clone on
|
||||
`FormFieldService::insertFromLibrary` (addendum Q3 row-copy mandate).
|
||||
- `toJsonShape(rules)` — single source of truth for serialising a
|
||||
collection to the canonical flat bag shape consumed by the snapshot
|
||||
writer (`FormSubmissionService::buildSnapshot`) and by API resources
|
||||
(`FormFieldResource`, `FormFieldLibraryResource`,
|
||||
`PublicFormSchemaResource`).
|
||||
- `assertSpecsValid(specs)` — public helper the FormRequests invoke in
|
||||
their `after()` hook to reject bad specs at the HTTP boundary before
|
||||
any write lands (strict validator on save, WS-5b commit 3).
|
||||
|
||||
**Cascade (`FormFieldChildTablesCascadeObserver`).** Shared observer —
|
||||
also cleans up `form_field_bindings` rows (WS-5a) and `form_field_configs`
|
||||
rows (WS-5b commit 5). Rules are physical state, not audit: on soft-
|
||||
or hard-delete of the owner, the observer physically deletes the rows.
|
||||
|
||||
**Activity log events.** Changing a field's rules emits two entries
|
||||
on the parent `FormField` subject:
|
||||
|
||||
- `field.updated` — payload includes `old.validation_rules` /
|
||||
`new.validation_rules` shapes reconstructed from the relational
|
||||
table via `FormFieldValidationRuleService::toJsonShape()`.
|
||||
Preserves the pre-WS-5b audit-consumer contract for downstream
|
||||
tooling that parses `field.updated` diffs.
|
||||
- `field.validation_rules_replaced` — the semantic rule-change event,
|
||||
emitted by `FormFieldValidationRuleService::replaceRules()`.
|
||||
|
||||
Both fire for the same semantic change. Aggregate queries over
|
||||
activity-log event counts should filter on one, not both — the WS-5a
|
||||
precedent (§6.7).
|
||||
|
||||
#### 17.4.3 Callback rules
|
||||
|
||||
`rule_type = callback` references a named handler registered in
|
||||
`config/form_builder.php`:
|
||||
|
||||
```php
|
||||
'validation_callbacks' => [
|
||||
'kvk_lookup' => \App\Services\Validators\KvkValidator::class,
|
||||
@@ -2223,7 +2360,55 @@ Registered callbacks in `config/form_builder.php`:
|
||||
],
|
||||
```
|
||||
|
||||
Unregistered callbacks rejected at save time.
|
||||
The rule row's `parameters.key` must be a key in this map — unregistered
|
||||
keys are rejected by `FormFieldValidationRuleService::assertSpecsValid()`
|
||||
at save time (WS-5b commit 3 strict validator).
|
||||
|
||||
**Legacy `FQCN@method` strings are not accepted.** Pre-WS-5b
|
||||
`validation_rules` JSON sometimes stored callbacks as fully-qualified
|
||||
class strings (`App\Services\Validators\KvkValidator@validate`). Those
|
||||
were never a formal contract and are not portable across refactors.
|
||||
Backfill surfaces them as WARN log lines for manual review; operators
|
||||
either register the callback under a named key in
|
||||
`config/form_builder.php` or drop the reference.
|
||||
|
||||
#### 17.4.4 Legacy JSON migration (WS-5b)
|
||||
|
||||
The WS-5b backfill migration
|
||||
(`2026_04_25_110001_backfill_form_field_validation_rules.php`)
|
||||
translates pre-WS-5b JSON keys to relational rows. Strict-enterprise
|
||||
dispatch — no guessing, no silent drops for unknown data.
|
||||
|
||||
- **Column-duplicates** (`required`, `unique`): WARN-log and skip.
|
||||
The is_* columns are the single sources of truth.
|
||||
- **Canonicalisations**: legacy `max_priorities` (SECTION_PRIORITY UI
|
||||
soft cap) collapses to `rule_type = max_selected` — same semantic of
|
||||
"cap on entries in a list-valued field", and two enum cases for one
|
||||
semantic is rot.
|
||||
- **Ambiguous `min` / `max`**: dispatched by `field_type`:
|
||||
- NUMBER → `min_value` / `max_value`
|
||||
- TEXT/TEXTAREA/EMAIL/PHONE/URL → `min_length` / `max_length`
|
||||
- DATE/DATETIME → `date_min` / `date_max`
|
||||
- anything else → **FAIL** the migration.
|
||||
Type-inappropriate uses of `min` / `max` are seed-data bugs.
|
||||
- **Non-validation keys** (`tag_categories`, `storage_disk`): skipped
|
||||
with an INFO log line — commit 5's configs-backfill migration picks
|
||||
them up into the separate `form_field_configs` table (ARCH §17.5).
|
||||
- **Unknown top-level keys**: **FAIL** the migration. Phase A seed-scan
|
||||
should have caught these; if one slips through we want the crash,
|
||||
not the skip.
|
||||
|
||||
Rollback reconstructs the JSON bag using canonical keys (post-rename).
|
||||
It does NOT resurrect column-duplicates or non-validation keys — those
|
||||
never landed in the relational table. The forward+back pair is safe as
|
||||
a unit; a partial rollback that pops this migration but leaves its
|
||||
create-table sibling is not a supported state.
|
||||
|
||||
Historical snapshots written pre-WS-5b embed the legacy flat bag
|
||||
(`validation_rules: {"min": 16, "max": 99, "max_priorities": 3}`).
|
||||
Those rows are immutable records and are not rewritten by the
|
||||
migration. Snapshot readers must tolerate both shapes — pre-WS-5b
|
||||
legacy keys and post-WS-5b canonical keys.
|
||||
|
||||
### 17.5 Webhooks
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
# Crewli — Core Database Schema
|
||||
|
||||
> Source: Design Document v1.3 — Section 3.5
|
||||
> **Version: 2.2** — Updated April 2026
|
||||
> **Version: 2.3** — Updated April 2026
|
||||
>
|
||||
> **Changelog:**
|
||||
>
|
||||
> - v2.3: WS-5b (partial) — `form_field_validation_rules` relational table
|
||||
> replaces the `validation_rules` JSON on `form_fields` and
|
||||
> `form_field_library`. Typed `rule_type` column + per-rule `parameters`
|
||||
> JSON; polymorphic morph owner reuses the WS-5a aliases (`form_field`,
|
||||
> `form_field_library`). Canonicalised legacy keys at backfill: ambiguous
|
||||
> `min`/`max` → `min_value`/`max_value`/`min_length`/`max_length`/
|
||||
> `date_min`/`date_max` by field type; `max_priorities` → `max_selected`.
|
||||
> Skipped keys: `required` (is_required column), `unique` (is_unique
|
||||
> column). Non-validation keys (`tag_categories`, `storage_disk`)
|
||||
> deferred to WS-5b commit 5 — they relocate to a separate
|
||||
> `form_field_configs` table (ARCH §17.5) rather than polluting the
|
||||
> validation-rules table. See ARCH-FORM-BUILDER.md §17.4 and
|
||||
> ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 §Q3 WS-5b Uitvoering.
|
||||
> - v2.2: WS-5a — `form_field_bindings` relational table replaces
|
||||
> `form_fields.binding` and `form_field_library.default_binding` JSON.
|
||||
> Polymorphic morph owner (`form_field` / `form_field_library`) per
|
||||
@@ -1967,7 +1980,6 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| `label` | string | |
|
||||
| `help_text` | text nullable | |
|
||||
| `options` | JSON nullable | Choice options |
|
||||
| `validation_rules` | JSON nullable | |
|
||||
| `default_is_required` | bool | default: false |
|
||||
| `default_is_filterable` | bool | default: false |
|
||||
| `translations` | JSON nullable | Per-locale overrides |
|
||||
@@ -1977,7 +1989,7 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| `is_active` | bool | default: true |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Relations:** `belongsTo` organisation; `hasMany` form_fields via `library_field_id`; `morphMany` form_field_bindings as `owner`
|
||||
**Relations:** `belongsTo` organisation; `hasMany` form_fields via `library_field_id`; `morphMany` form_field_bindings as `owner`; `morphMany` form_field_validation_rules as `owner`
|
||||
**Indexes:** `(organisation_id, field_type)`, `(organisation_id, is_active)`
|
||||
**Unique constraint:** `UNIQUE(organisation_id, slug)`
|
||||
**Global scope:** `OrganisationScope`
|
||||
@@ -1986,6 +1998,11 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
> Bindings moved to the relational `form_field_bindings` table
|
||||
> (ARCH-FORM-BUILDER.md §6.7). See WS-5a in
|
||||
> `/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md` §Q3.
|
||||
>
|
||||
> Validation rules moved to the relational `form_field_validation_rules`
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -2012,7 +2029,6 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
| `help_text` | text nullable | |
|
||||
| `section` | string(100) null | Visual grouping header (independent of `form_schema_section_id`) |
|
||||
| `options` | JSON nullable | Choice options |
|
||||
| `validation_rules` | JSON nullable | min/max/regex/allowed_mime_types/storage_disk/callback, etc. |
|
||||
| `is_required` | bool | default: false |
|
||||
| `is_filterable` | bool | default: false — populates `form_values.value_indexed` / pivot |
|
||||
| `is_portal_visible` | bool | default: true |
|
||||
@@ -2029,13 +2045,18 @@ 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`
|
||||
**Relations:** `belongsTo` schema, section (nullable), libraryField; `hasMany` form_values; `morphMany` form_field_bindings as `owner`; `morphMany` form_field_validation_rules as `owner`
|
||||
**Indexes:** `(form_schema_id, sort_order)`, `(form_schema_id, is_filterable)`, `(library_field_id)`, `(form_schema_id, slug)`
|
||||
**Soft delete:** yes
|
||||
|
||||
> Bindings moved to the relational `form_field_bindings` table
|
||||
> (ARCH-FORM-BUILDER.md §6.7). See WS-5a in
|
||||
> `/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md` §Q3.
|
||||
>
|
||||
> Validation rules moved to the relational `form_field_validation_rules`
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -2071,6 +2092,38 @@ that aggregates the user's submitted, non-test `form_submissions`.
|
||||
|
||||
---
|
||||
|
||||
### `form_field_validation_rules`
|
||||
|
||||
> Relational home for field and library-field validation rules. One row
|
||||
> per `(owner, rule_type)`. `rule_type` is an app-enforced enum
|
||||
> (`FormFieldValidationRuleType`) — database column is `string(40)` so
|
||||
> the enum can extend without migration. `parameters` JSON carries per-
|
||||
> rule-type configuration (value, pattern, mime_types, etc.); shape is
|
||||
> enforced at the service layer, not the DB.
|
||||
>
|
||||
> Polymorphic owner — morph-map aliases `form_field` and
|
||||
> `form_field_library` (reused from WS-5a). Rules are physical state
|
||||
> (not audit) — they cascade on owner delete (soft or hard) via the
|
||||
> shared `FormFieldChildTablesCascadeObserver`. ARCH §17.4.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------- | ----------------- | ------------------------------------------------------------------------------ |
|
||||
| `id` | ULID | PK |
|
||||
| `owner_type` | string(40) | morph alias: `form_field` or `form_field_library` |
|
||||
| `owner_id` | ULID | parent row (a `form_fields.id` or `form_field_library.id`) |
|
||||
| `rule_type` | string(40) | `FormFieldValidationRuleType` case (`min_length`, `max_value`, `regex`, `allowed_mime_types`, `callback`, etc.) |
|
||||
| `parameters` | JSON | Per-rule-type bag (e.g. `{"value": 3}` for `min_length`, `{"mime_types":[...]}` for `allowed_mime_types`) |
|
||||
| `error_message_key` | string(100) null | Optional i18n key for custom rejection message |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Relations:** `morphTo` owner (`form_field` or `form_field_library`)
|
||||
**Indexes:** `(rule_type)`, `(owner_type, owner_id)`
|
||||
**Unique constraint:** `UNIQUE(owner_type, owner_id, rule_type)`
|
||||
**Global scope:** `FormFieldValidationRuleScope` — sibling to `FormFieldBindingScope`, same UNION shape over both owner chains. Escape hatch: `withoutGlobalScope(FormFieldValidationRuleScope::class)`. Base-class extraction deferred to WS-5d per addendum Q3 (premature abstraction from two siblings is still premature).
|
||||
**Soft delete:** no — rules are current state, not audit
|
||||
|
||||
---
|
||||
|
||||
### `form_submissions`
|
||||
|
||||
> One submission per `(schema, subject)` in `single` / `draft_single`
|
||||
|
||||
Reference in New Issue
Block a user