refactor(form-builder): strict validator + drop form_fields.conditional_logic JSON column
WS-5c commit 3 of 4. FormRequests (Store/Update) now reject bad
conditional_logic trees at the HTTP boundary — the `after()` hook
unwraps the `show_when` envelope, normalises legacy `{all|any: [...]}`
group shape to the service's internal form, and delegates to
`FormFieldConditionalLogicService::assertSpecsValid()`. Unknown
operators, root conditions, empty groups, and unknown field_slug
references produce a 422 with a readable error before any write.
`form_fields.conditional_logic` JSON column dropped. FormField model
`$fillable` and `$casts` no longer mention the column; factory default
no longer writes `null` to it. Snapshot fixtures in the dev seeder and
the legacy-forms migration command keep `conditional_logic` in their
snapshot JSON shape — that's the schema_snapshot contract, not the DB
column.
FormFieldController now maps InvalidConditionalLogicSpecException to
422 alongside FrozenSchemaException / CyclicDependencyException.
Rollback path: roll back WS-5c commits 1–3 together. Partial rollback
(drop-column reversed but backfill still applied) is not a supported
state — matching the WS-5a/b precedent on the family's full-rollback
contract.
Tests: 6 new (strict FormRequest rejection cases + JSON-column drop
assertion). Rollback step counts in WS-5a/b migration tests bumped +1
for the drop_conditional_logic_json_column migration. Baseline
1142 → 1148 green (3085 → 3099 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,6 @@ final class FormFieldFactory extends Factory
|
||||
'is_unique' => false,
|
||||
'is_pii' => false,
|
||||
'display_width' => FormFieldDisplayWidth::FULL,
|
||||
'conditional_logic' => null,
|
||||
'role_restrictions' => null,
|
||||
'translations' => null,
|
||||
'value_storage_hint' => $fieldType->recommendedValueStorageHint(),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* WS-5c commit 3 — drops the `conditional_logic` JSON column on
|
||||
* `form_fields`. By this point WS-5c commit 2's backfill has populated
|
||||
* `form_field_conditional_logic_groups` / `form_field_conditional_logic
|
||||
* _conditions` from the source JSON, and every reader (snapshot writer,
|
||||
* API resources, FormFieldService cycle check) has been switched to the
|
||||
* relational tables.
|
||||
*
|
||||
* Per addendum Q3, conditional_logic only applies to FormField — there is
|
||||
* no library mirror to drop.
|
||||
*
|
||||
* Rollback re-adds the column as `json nullable` **without** backfilling
|
||||
* — the rollback path is "roll back WS-5c commits 1–3 together". After
|
||||
* this migration's `down()` the commit 2 backfill's `down()` hook
|
||||
* reconstructs the source JSON bag.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('form_fields', 'conditional_logic')) {
|
||||
Schema::table('form_fields', function (Blueprint $table): void {
|
||||
$table->dropColumn('conditional_logic');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasColumn('form_fields', 'conditional_logic')) {
|
||||
Schema::table('form_fields', function (Blueprint $table): void {
|
||||
$table->json('conditional_logic')->nullable()->after('display_width');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -100,7 +100,7 @@ final class FormBuilderDevSeeder
|
||||
'is_filterable' => $field['is_filterable'] ?? false,
|
||||
'is_pii' => $field['is_pii'] ?? false,
|
||||
'binding' => null, // Pattern B — snapshot embeds null for form-owned fields.
|
||||
'conditional_logic' => null,
|
||||
'conditional_logic' => null, // snapshot shape: null for fields without conditional logic
|
||||
'translations' => null,
|
||||
'value_storage_hint' => $field['type']->recommendedValueStorageHint()->value,
|
||||
'sort_order' => $sortOrder + 1,
|
||||
|
||||
Reference in New Issue
Block a user