feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* WS-5b commit 5 of 5 — parallel relational table to
|
||||
* `form_field_validation_rules` that holds non-validation field
|
||||
* configuration (tag_categories, storage_disk). Same polymorphic
|
||||
* owner pattern; semantically distinct concern (ARCH-FORM-BUILDER
|
||||
* §17.5; addendum Q3 WS-5b Uitvoering).
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_field_configs', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->string('owner_type', 40);
|
||||
$table->ulid('owner_id');
|
||||
$table->string('config_type', 40);
|
||||
$table->json('parameters');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(
|
||||
['owner_type', 'owner_id', 'config_type'],
|
||||
'ffc_owner_config_unique',
|
||||
);
|
||||
$table->index('config_type', 'ffc_config_idx');
|
||||
$table->index(['owner_type', 'owner_id'], 'ffc_owner_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_field_configs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\FormBuilder\FormFieldConfigType;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* WS-5b commit 5 — translates the non-validation keys that WS-5b
|
||||
* commit 2's backfill deliberately skipped (`tag_categories`,
|
||||
* `storage_disk`) into rows in `form_field_configs`.
|
||||
*
|
||||
* Reads from the pre-drop JSON columns (`form_fields.validation_rules`,
|
||||
* `form_field_library.validation_rules`). The sibling drop-migration
|
||||
* (`2026_04_25_120002`) runs after this one; rolling back WS-5b
|
||||
* commits 1–5 as a unit reconstructs source JSON on the source tables
|
||||
* using canonical keys before the table is dropped.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::transaction(function (): void {
|
||||
$this->backfill('form_fields', 'form_field');
|
||||
$this->backfill('form_field_library', 'form_field_library');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('form_field_configs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function (): void {
|
||||
$this->reconstructJson('form_fields', 'form_field');
|
||||
$this->reconstructJson('form_field_library', 'form_field_library');
|
||||
});
|
||||
}
|
||||
|
||||
private function backfill(string $table, string $ownerType): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'validation_rules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = DB::table($table)
|
||||
->whereNotNull('validation_rules')
|
||||
->orderBy('id')
|
||||
->get(['id', 'validation_rules']);
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$inserts = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$decoded = is_string($row->validation_rules)
|
||||
? json_decode((string) $row->validation_rules, true)
|
||||
: $row->validation_rules;
|
||||
if (! is_array($decoded) || $decoded === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($decoded['tag_categories']) && is_array($decoded['tag_categories'])) {
|
||||
$inserts[] = [
|
||||
'id' => (string) Str::ulid(),
|
||||
'owner_type' => $ownerType,
|
||||
'owner_id' => (string) $row->id,
|
||||
'config_type' => FormFieldConfigType::TagCategories->value,
|
||||
'parameters' => json_encode([
|
||||
'categories' => array_values(array_map(
|
||||
static fn ($c): string => (string) $c,
|
||||
$decoded['tag_categories'],
|
||||
)),
|
||||
]),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($decoded['storage_disk']) && is_string($decoded['storage_disk']) && $decoded['storage_disk'] !== '') {
|
||||
$inserts[] = [
|
||||
'id' => (string) Str::ulid(),
|
||||
'owner_type' => $ownerType,
|
||||
'owner_id' => (string) $row->id,
|
||||
'config_type' => FormFieldConfigType::StorageDisk->value,
|
||||
'parameters' => json_encode(['disk' => $decoded['storage_disk']]),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($inserts === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_chunk($inserts, 500) as $batch) {
|
||||
DB::table('form_field_configs')->insert($batch);
|
||||
}
|
||||
}
|
||||
|
||||
private function reconstructJson(string $table, string $ownerType): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'validation_rules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = DB::table('form_field_configs')
|
||||
->where('owner_type', $ownerType)
|
||||
->orderBy('owner_id')
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$grouped = [];
|
||||
foreach ($rows as $row) {
|
||||
$ownerId = (string) $row->owner_id;
|
||||
$grouped[$ownerId] ??= [];
|
||||
$params = json_decode((string) $row->parameters, true);
|
||||
$params = is_array($params) ? $params : [];
|
||||
|
||||
if ($row->config_type === FormFieldConfigType::TagCategories->value) {
|
||||
$grouped[$ownerId]['tag_categories'] = $params['categories'] ?? [];
|
||||
} elseif ($row->config_type === FormFieldConfigType::StorageDisk->value) {
|
||||
$grouped[$ownerId]['storage_disk'] = $params['disk'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($grouped as $ownerId => $configs) {
|
||||
$existing = DB::table($table)->where('id', $ownerId)->value('validation_rules');
|
||||
$existingBag = is_string($existing) ? (json_decode($existing, true) ?: []) : [];
|
||||
$merged = array_merge(is_array($existingBag) ? $existingBag : [], $configs);
|
||||
|
||||
DB::table($table)->where('id', $ownerId)->update([
|
||||
'validation_rules' => json_encode($merged),
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* WS-5b commit 5 — drops the `validation_rules` JSON columns on
|
||||
* `form_fields` and `form_field_library`. By this point both WS-5b
|
||||
* backfills (validation-rules in commit 2, configs in commit 5) have
|
||||
* populated their respective relational tables from the source JSON.
|
||||
*
|
||||
* Rollback re-adds the columns as `json nullable` **without** backfilling
|
||||
* — the rollback path is "roll back WS-5b commits 1–5 together". After
|
||||
* this migration's `down()` the two backfill migrations' `down()` hooks
|
||||
* reconstruct the source JSON bags.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('form_fields', 'validation_rules')) {
|
||||
Schema::table('form_fields', function (Blueprint $table): void {
|
||||
$table->dropColumn('validation_rules');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('form_field_library', 'validation_rules')) {
|
||||
Schema::table('form_field_library', function (Blueprint $table): void {
|
||||
$table->dropColumn('validation_rules');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasColumn('form_fields', 'validation_rules')) {
|
||||
Schema::table('form_fields', function (Blueprint $table): void {
|
||||
$table->json('validation_rules')->nullable()->after('options');
|
||||
});
|
||||
}
|
||||
if (! Schema::hasColumn('form_field_library', 'validation_rules')) {
|
||||
Schema::table('form_field_library', function (Blueprint $table): void {
|
||||
$table->json('validation_rules')->nullable()->after('options');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user