feat(form-field): backfill form_fields.options to form_field_options
Atomic data migration. Every options datum in the database — in
form_fields and form_field_library, their translations bags, and the
form_submissions.schema_snapshot + form_templates.schema_snapshot JSON
blobs — is converted to the new relational rich-shape representation.
Strict dispatch per §17.4.4 / §8.7 convention:
- Fail on field_type ∉ {RADIO, SELECT, MULTISELECT, CHECKBOX_LIST}
carrying non-null options (post-WS-5b TAG_PICKER seed-bug indicator)
- Fail on non-flat-string-array options shape
- Fail on translations.{locale}.options[] length mismatch
- Fail on non-string / >255-char translated labels
- Fail on any residual translations.{locale}.options key after
step C migration
Snapshot rewrite in-place: both form_submissions.schema_snapshot and
form_templates.schema_snapshot walk fields[*] and rewrite options to
the new rich-shape, strip per-locale options[] from the parallel
translations bag. Zero-compromise directive — no reader tolerance for
pre-WS-5d shape in commit 3 onwards.
Rollback reconstructs JSON column shapes plus translations bags.
Forward+back pair safe as a unit; partial rollback unsupported.
FormFieldService::insertFromLibrary switches from JSON-copy to
FormFieldOptionService::copyOptions row-clone per addendum Q3 row-copy
mandate. The field's own translations bag no longer carries
{locale}.options keys — those live on option rows now.
Seeders and factories switch to service-level option creation:
- FormBuilderDevSeeder.canonicalFields keeps flat-string options as
its data shape; FormField::create no longer receives an options
key, the post-create FormFieldOptionService::replaceOptions call
inserts the rich rows. The same applies to
seedEventRegistrationShowcaseSchema. The vergoedingstype field's
legacy {label, description} object shape (a pre-WS-5d seed-bug
that the strict backfill would reject) is normalised to flat
strings; the descriptions are dropped.
- seedSystemTemplates embeds rich-shape options in the template
snapshot — no flat-array snapshot data remains in newly-seeded
rows.
- FormFieldFactory + FormFieldLibraryFactory drop the options
default; new ::withOptions() helper accepts either flat strings
(each becomes value+label) or full spec arrays and routes through
the service.
JSON columns (form_fields.options, form_field_library.options) remain
present and writable via fillable; column-drop lands in commit 5.
Reads from the JSON column still exist in resources, snapshot writer,
FormRequests, FormValueService, and FilterRegistryController — commit
3 switches those all atomically.
Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new backfill_form_field_options migration on the migration stack.
Tests: 1182 → 1193 green (+11 tests / +56 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,14 +33,14 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
|
||||
public function test_forward_migrations_backfill_rows_from_both_json_sources(): void
|
||||
{
|
||||
// Roll back to pre-WS-5a state: 1 WS-5d migration (create-options) +
|
||||
// 4 WS-5c migrations (drop-conditional-logic-col,
|
||||
// Roll back to pre-WS-5a state: 2 WS-5d migrations (backfill-options,
|
||||
// create-options) + 4 WS-5c migrations (drop-conditional-logic-col,
|
||||
// backfill-conditional-logic, create-conditional-logic-conditions,
|
||||
// create-conditional-logic-groups) + 5 WS-5b migrations
|
||||
// (drop-validation-cols, configs-backfill, create-configs,
|
||||
// validation-rules-backfill, create-validation-rules) +
|
||||
// 2 WS-5a migrations (drop-binding-cols, create-bindings) = 12.
|
||||
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
|
||||
// 2 WS-5a migrations (drop-binding-cols, create-bindings) = 13.
|
||||
$this->artisan('migrate:rollback', ['--step' => 13])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_bindings'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'binding'));
|
||||
$this->assertTrue(Schema::hasColumn('form_field_library', 'default_binding'));
|
||||
@@ -101,8 +101,8 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
|
||||
public function test_rollback_reconstructs_json_and_drops_table(): void
|
||||
{
|
||||
// Walk back the full WS-5d + WS-5c + WS-5b + WS-5a stack (12 migrations).
|
||||
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
|
||||
// Walk back the full WS-5d + WS-5c + WS-5b + WS-5a stack (13 migrations).
|
||||
$this->artisan('migrate:rollback', ['--step' => 13])->assertSuccessful();
|
||||
[$fieldAId, , ] = $this->seedFieldsWithBindingJson();
|
||||
[$libAId, ] = $this->seedLibraryWithBindingJson();
|
||||
|
||||
@@ -112,12 +112,12 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
$this->assertFalse(Schema::hasColumn('form_fields', 'binding'));
|
||||
$this->assertSame(5, DB::table('form_field_bindings')->count());
|
||||
|
||||
// Step back over WS-5d (1 migration) + WS-5c (4 migrations) +
|
||||
// Step back over WS-5d (2 migrations) + WS-5c (4 migrations) +
|
||||
// WS-5b (5 migrations) in one go → restores the pre-WS-5b state
|
||||
// (conditional-logic, validation-rules, configs and options tables
|
||||
// gone, validation_rules JSON columns reappear on source tables;
|
||||
// binding contract intact).
|
||||
$this->artisan('migrate:rollback', ['--step' => 10])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 11])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_options'));
|
||||
$this->assertFalse(Schema::hasTable('form_field_conditional_logic_groups'));
|
||||
$this->assertFalse(Schema::hasTable('form_field_conditional_logic_conditions'));
|
||||
|
||||
Reference in New Issue
Block a user