refactor(form-field): drop form_fields.options + form_field_library.options
Final WS-5d cleanup. The JSON columns that have been unread since
commit 3 are now physically dropped on both source tables. Their
canonical rich-shape lives in form_field_options, accessed
exclusively through the morphMany relation.
Defensive sweep: any lingering translations.{locale}.options key in
either source table's translations bag is stripped. Commit 2's
backfill should already have done so exhaustively; this is
belt-and-braces.
Rollback re-creates the columns as nullable JSON but leaves them
empty. Pair with commit 2's rollback to restore the pre-WS-5d data
shape on every owner row.
The commit-3 getOptionsAttribute accessor-bridge on FormField +
FormFieldLibrary is removed — Eloquent's getAttribute() resolution
now naturally falls through to the morphMany relation since there's
no underlying column to shadow it. New regression test
FormFieldOptionsAccessTest asserts $field->options resolves to an
Eloquent Collection of FormFieldOption instances and lazy-loads in
exactly 2 queries (1 parent + 1 lazy-load options) on a fresh fetch
without with() preload. Same trio for FormFieldLibrary.
Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new drop_form_field_options_json_columns migration on the
rollback stack.
Documentation:
- SCHEMA.md v2.6: form_field_options table documented; options row
removed from form_fields and form_field_library; morphMany
relations updated; cross-references to ARCH-FORM-BUILDER §17.6
and addendum §Q3 WS-5d Uitvoering added on both source-table
docblocks.
- ARCH-FORM-BUILDER.md v1.8: new §17.6 "Field options (relational)"
mirrors the §17.4 / §17.5 relational-sibling structure with
sub-sections 17.6.1 rationale, 17.6.2 table + catalogue, 17.6.3
service / scope / cascade / activity log, 17.6.4 snapshot
embedding, 17.6.5 external API contract. Existing Webhooks
section renumbered from §17.6 to §17.7.
- ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md: "Uitvoering — WS-5d
(2026-04-27)" section added. Eight paragraphs covering the
snapshot atomic rewrite, strict-fail backfill dispatch, dual
activity-log emit, four-sibling base-class extraction warrant,
commit 0 dead-code precondition, the temporary getOptionsAttribute
accessor-bridge pattern (with reusability note for future
JSON→relational refactors), the dev-seeder vergoedingstype RADIO
normalisation (drift correction explicitly distinguished from the
parallel apps/app RegistrationFieldTemplate description domain),
and the WS-5 family completion note.
- BACKLOG.md: FORM-BUILDER-LIBRARY-AUDIT-LOG entry extended to four
services (adds library.options_replaced); new
FORM-BUILDER-MORPH-SCOPE-BASE-CLASS entry added as the WS-5d
follow-up now that all four concrete morph-scope siblings exist.
Tests: 1193 → 1208 green (+15 across commits 3+4+5; this commit alone:
+2 from the regression test).
This completes the WS-5 family.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# ARCH — Universal Form Builder (v1.7)
|
||||
# ARCH — Universal Form Builder (v1.8)
|
||||
|
||||
> **Source of truth** for Crewli's universal Form Builder architecture.
|
||||
> Any discrepancy with SCHEMA.md is resolved in favour of this document
|
||||
@@ -10,11 +10,18 @@
|
||||
> columns dropped); WS-5c landed (relational
|
||||
> `form_field_conditional_logic_groups` + `form_field_conditional_logic_conditions`;
|
||||
> pre-WS-5c `conditional_logic` JSON column dropped; no library mirror
|
||||
> per addendum Q3).
|
||||
> **Version:** 1.7 (§8 restructured into tree-structure, relational-tables,
|
||||
> service-boundary, operator-catalogue, cycle-detection, activity-log and
|
||||
> legacy-migration sub-sections; contract unchanged).
|
||||
> per addendum Q3); WS-5d landed (relational `form_field_options`;
|
||||
> pre-WS-5d `options` JSON columns dropped on both `form_fields` and
|
||||
> `form_field_library`; per-option translations live on the option row
|
||||
> itself). **WS-5 family complete.**
|
||||
> **Version:** 1.8 (new §17.6 "Field options (relational)" for the WS-5d
|
||||
> split; §17.4 / §17.5 sibling-catalogue prose extended to mention the
|
||||
> fourth concrete morph-scope; existing Webhooks section renumbered
|
||||
> from §17.6 to §17.7).
|
||||
> **Previous:**
|
||||
> 1.7 (§8 restructured into tree-structure, relational-tables,
|
||||
> service-boundary, operator-catalogue, cycle-detection, activity-log
|
||||
> and legacy-migration sub-sections; contract unchanged),
|
||||
> 1.6 (new §17.5 "Field configuration (non-validation)" for
|
||||
> the `form_field_configs` split; §17.4.4 updated with the
|
||||
> non-validation-key relocation note),
|
||||
@@ -2667,13 +2674,138 @@ The portal + organizer SPAs are updated in the same work package
|
||||
|
||||
---
|
||||
|
||||
### 17.6 Webhooks
|
||||
### 17.6 Field options (relational)
|
||||
|
||||
#### 17.6.1 Schema
|
||||
#### 17.6.1 Rationale
|
||||
|
||||
Pre-WS-5d, `form_fields.options` and `form_field_library.options` were
|
||||
JSON columns of flat string arrays consumed by RADIO / SELECT /
|
||||
MULTISELECT / CHECKBOX_LIST. The shape conflated three distinct
|
||||
concerns: the canonical storage value, the default-locale display
|
||||
label, and per-locale translations (which lived elsewhere as the
|
||||
parallel `translations.{locale}.options[]` indexed array). WS-5d
|
||||
splits the bag into one polymorphic relational table where each row is
|
||||
a single option carrying value, label, sort_order and an optional
|
||||
per-locale translations JSON map.
|
||||
|
||||
WS-5d follows the WS-5a / WS-5b discipline one-for-one: dedicated
|
||||
service as single writer, UNION-over-two-owner-chains scope, shared
|
||||
cascade observer. Fourth and final WS-5 sibling — landing it
|
||||
materialises the four concrete morph-scope implementations and
|
||||
unblocks the deliberate follow-up of base-class extraction.
|
||||
|
||||
#### 17.6.2 Table + catalogue
|
||||
|
||||
Single table `form_field_options` carrying:
|
||||
|
||||
- `value` — string ≤255 chars, the canonical storage value used by the
|
||||
`in:options` validator and embedded in `form_values` rows. UNIQUE per
|
||||
owner.
|
||||
- `label` — string ≤255 chars, the default-locale display label.
|
||||
- `sort_order` — int unsigned, stable ordering within owner.
|
||||
- `translations` — JSON nullable, `{<locale>: <translated label>}` with
|
||||
BCP-47 short-form locale keys (`nl`, `en`, `nl_BE`, `en_GB`).
|
||||
|
||||
Polymorphic owner: morph aliases `form_field` and `form_field_library`,
|
||||
reused from WS-5a. UNIQUE index `ffo_owner_value_unique` on
|
||||
`(owner_type, owner_id, value)` is the seed-bug guard — duplicate
|
||||
values per field have no semantic meaning and must fail at both the
|
||||
service layer (`assertSpecsValid`) and the DB level. Sort-order index
|
||||
`ffo_owner_sort_idx` on `(owner_type, owner_id, sort_order)` for
|
||||
ordered fetches.
|
||||
|
||||
Applies only to field types that consume options:
|
||||
**RADIO / SELECT / MULTISELECT / CHECKBOX_LIST**. TAG_PICKER's category
|
||||
filter lives in `form_field_configs` (§17.5);
|
||||
AVAILABILITY_PICKER and SECTION_PRIORITY source options dynamically
|
||||
from sibling endpoints. Any other field type carrying non-null options
|
||||
in pre-WS-5d data is a seed bug and the strict-fail backfill rejects
|
||||
it.
|
||||
|
||||
#### 17.6.3 Service / scope / cascade / activity log
|
||||
|
||||
`FormFieldOptionService` is the single writer. Public surface:
|
||||
|
||||
- `optionsFor(owner)` — eager, ordered by sort_order
|
||||
- `replaceOptions(owner, specs)` — transactional: validate spec list,
|
||||
delete prior rows, insert new rows. Returns the fresh collection.
|
||||
- `copyOptions(from, to)` — pure row-clone for
|
||||
`FormFieldService::insertFromLibrary` per the addendum Q3 row-copy
|
||||
mandate. No activity-log emit (the wrapping field-creation event
|
||||
carries the audit).
|
||||
- `toJsonShape(collection)` — serialises to the rich-shape array used
|
||||
by snapshot writer, API resources and `FilterRegistryController`.
|
||||
- `assertSpecsValid(specs)` — public spec-shape gate, used by
|
||||
FormRequests in their `after()` hook to reject malformed specs at the
|
||||
HTTP boundary before any write.
|
||||
|
||||
`FormFieldOptionScope` is the fourth concrete UNION-over-two-owner-
|
||||
chains sibling, near-duplicate of the binding / validation-rules /
|
||||
configs scopes. Base-class extraction across the four siblings is
|
||||
deliberately deferred to a follow-up work package now that the four
|
||||
implementations exist and the "what actually varies" question can be
|
||||
answered empirically.
|
||||
|
||||
`FormFieldChildTablesCascadeObserver` extended to physically delete
|
||||
option rows on owner soft-delete OR force-delete; options are
|
||||
physical state, not audit (submission snapshots carry the historical
|
||||
shape).
|
||||
|
||||
Activity log dual-emit on FormField subject only (mirrors §6.7 /
|
||||
§17.4.2):
|
||||
|
||||
- `field.updated` carries `old.options` / `new.options` diff via
|
||||
`toJsonShape()` reconstruction. The diff is byte-equal JSON-compared
|
||||
to skip cosmetic false positives — bare label/sort_order updates
|
||||
that don't touch options omit the key entirely.
|
||||
- `field.options_replaced` is the semantic event from
|
||||
`replaceOptions()`, payload `{options: [...rich shape...]}`.
|
||||
|
||||
Library-subject writes are silent in activity log (consistent with
|
||||
WS-5a / WS-5b convention; library audits live elsewhere).
|
||||
|
||||
#### 17.6.4 Snapshot embedding
|
||||
|
||||
`FormSubmissionService::buildSnapshot` walks `fields[*]` and emits
|
||||
options through `FormFieldOptionService::toJsonShape()` in the same
|
||||
rich shape exposed by API resources. The pre-WS-5d
|
||||
`translations.{locale}.options[]` parallel arrays are dead — option
|
||||
translations live on each option row's own `translations` JSON. The
|
||||
field-snapshot's translations bag retains only `{label, help_text}`
|
||||
per locale. WS-5d commit 2's backfill rewrote every existing
|
||||
submission + template snapshot in-place; no historical flat-array
|
||||
options remain post-commit-2.
|
||||
|
||||
#### 17.6.5 External API contract (no bridging)
|
||||
|
||||
Resources, snapshot writer, and `FilterRegistryController` emit
|
||||
options uniformly as the rich shape:
|
||||
|
||||
```json
|
||||
[
|
||||
{"value": "red", "label": "Red", "sort_order": 0,
|
||||
"translations": {"nl": "Rood"}},
|
||||
{"value": "green", "label": "Green", "sort_order": 1}
|
||||
]
|
||||
```
|
||||
|
||||
Empty option set serialises as `null` (preserves the option-less
|
||||
field-type contract). Per ARCH-FORM-BUILDER §0 "Breaking change
|
||||
acceptance", the portal SPA was migrated atomically in WS-5d commit 4
|
||||
with no flat-array carve-out. Downstream consumers wanting the raw
|
||||
value list extract `options.map(o => o.value)`; consumers wanting
|
||||
Vuetify-style `{value, title}` pairs use `resolveOptionLabel(option,
|
||||
locale)` from `@form-schema/types/formBuilder` and map over.
|
||||
|
||||
---
|
||||
|
||||
### 17.7 Webhooks
|
||||
|
||||
#### 17.7.1 Schema
|
||||
|
||||
See §4.11 `form_schema_webhooks` and §4.12 `form_webhook_deliveries`.
|
||||
|
||||
#### 17.6.2 Dispatcher
|
||||
#### 17.7.2 Dispatcher
|
||||
|
||||
`FormWebhookDispatcher` listens for FormSubmissionSubmitted / Reviewed /
|
||||
SectionSubmitted / SectionReviewed events. On trigger:
|
||||
@@ -2681,7 +2813,7 @@ SectionSubmitted / SectionReviewed events. On trigger:
|
||||
- For each: creates a form_webhook_delivery row with status=pending
|
||||
- Queues `DeliverFormWebhookJob` per delivery on dedicated `webhooks` queue
|
||||
|
||||
#### 17.6.3 Delivery job
|
||||
#### 17.7.3 Delivery job
|
||||
|
||||
`DeliverFormWebhookJob` on `webhooks` queue:
|
||||
- Idempotent (Laravel job with unique ID per delivery)
|
||||
@@ -2700,7 +2832,7 @@ SectionSubmitted / SectionReviewed events. On trigger:
|
||||
Response body first 1000 chars stored in `response_body_excerpt` for
|
||||
debugging.
|
||||
|
||||
#### 17.6.4 Security
|
||||
#### 17.7.4 Security
|
||||
|
||||
URL validation in `FormWebhookDispatcher`:
|
||||
- Parse URL; reject non-http(s)
|
||||
@@ -2712,7 +2844,7 @@ URL validation in `FormWebhookDispatcher`:
|
||||
|
||||
Admin UI shows validation status + last delivery attempt per webhook.
|
||||
|
||||
#### 17.6.5 Webhook payload format
|
||||
#### 17.7.5 Webhook payload format
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user