feat(forms): add core migrations (user_profiles, schemas, fields, submissions, values, webhooks)
14 fresh tables per ARCH §4 (v1.2): user_profiles + 13 form_* tables. ULID PKs on domain rows, integer AI on heavy-join EAV tables (form_values, form_value_options). All FKs indexed, every constraint named explicitly. FULLTEXT on form_submissions.search_index is best-effort (wrapped try/catch so SQLite test runs still apply). Notes: - Partial unique indexes on public_token/custom_purpose_slug traded for regular indexes + application-level uniqueness (MySQL limitation). - (form_schema_id, slug) on form_fields is a regular index to avoid soft-delete + re-create collisions; uniqueness enforced in service. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_profiles', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('user_id')->unique()->constrained()->cascadeOnDelete();
|
||||
$table->text('bio')->nullable();
|
||||
$table->string('photo_url')->nullable();
|
||||
$table->string('emergency_contact_name')->nullable();
|
||||
$table->string('emergency_contact_phone')->nullable();
|
||||
$table->decimal('reliability_score', 3, 2)->default(0.00);
|
||||
$table->boolean('is_ambassador')->default(false);
|
||||
$table->json('settings')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('reliability_score', 'user_profiles_reliability_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_profiles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('users')->orderBy('id')->chunkById(100, function ($users): void {
|
||||
$existing = DB::table('user_profiles')
|
||||
->whereIn('user_id', $users->pluck('id'))
|
||||
->pluck('user_id')
|
||||
->all();
|
||||
|
||||
$rows = $users
|
||||
->reject(fn ($u) => in_array($u->id, $existing, true))
|
||||
->map(fn ($u) => [
|
||||
'id' => (string) Str::ulid(),
|
||||
'user_id' => $u->id,
|
||||
'reliability_score' => 0.00,
|
||||
'is_ambassador' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (! empty($rows)) {
|
||||
DB::table('user_profiles')->insert($rows);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Destructive: truncate all user_profiles rows created here.
|
||||
// down() for this migration only makes sense together with the
|
||||
// preceding create_user_profiles_table rollback.
|
||||
DB::table('user_profiles')->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_schemas', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
// Polymorphic owner (event / user_profile / artist / company / null)
|
||||
$table->string('owner_type', 50)->nullable();
|
||||
$table->ulid('owner_id')->nullable();
|
||||
|
||||
$table->string('name');
|
||||
$table->string('slug');
|
||||
$table->string('purpose', 50);
|
||||
$table->string('custom_purpose_slug')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->string('submission_mode', 20);
|
||||
|
||||
$table->ulid('public_token')->nullable();
|
||||
$table->ulid('public_token_previous')->nullable();
|
||||
$table->timestamp('public_token_rotated_at')->nullable();
|
||||
|
||||
$table->timestamp('submission_deadline')->nullable();
|
||||
$table->string('locale', 10)->default('nl');
|
||||
$table->json('settings')->nullable();
|
||||
|
||||
$table->unsignedInteger('version')->default(1);
|
||||
$table->string('snapshot_mode', 20)->default('never');
|
||||
$table->boolean('freeze_on_submit')->default(false);
|
||||
$table->unsignedInteger('retention_days')->nullable();
|
||||
$table->string('consent_version')->nullable();
|
||||
$table->boolean('section_level_submit')->default(false);
|
||||
$table->boolean('auto_save_enabled')->default(false);
|
||||
$table->unsignedInteger('max_submissions')->nullable();
|
||||
|
||||
$table->foreignUlid('created_by_user_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->foreignUlid('last_updated_by_user_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->foreignUlid('edit_lock_user_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('edit_lock_expires_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['organisation_id', 'purpose'], 'fs_org_purpose_idx');
|
||||
$table->index(['owner_type', 'owner_id'], 'fs_owner_idx');
|
||||
$table->unique(['organisation_id', 'slug'], 'fs_org_slug_unique');
|
||||
|
||||
// MySQL cannot express a partial unique index cross-platform.
|
||||
// We use regular indexes; application-level uniqueness enforced
|
||||
// in FormSchemaService (NULLs bypass DB-level uniqueness anyway).
|
||||
$table->index('public_token', 'fs_public_token_idx');
|
||||
$table->index('public_token_previous', 'fs_public_token_prev_idx');
|
||||
$table->index('custom_purpose_slug', 'fs_custom_purpose_slug_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_schemas');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_schema_sections', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('form_schema_id')
|
||||
->constrained('form_schemas')
|
||||
->cascadeOnDelete();
|
||||
$table->string('slug');
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->boolean('submit_independent')->default(true);
|
||||
$table->foreignUlid('depends_on_section_id')
|
||||
->nullable()
|
||||
->constrained('form_schema_sections')
|
||||
->nullOnDelete();
|
||||
$table->boolean('required_for_schema_submit')->default(true);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['form_schema_id', 'sort_order'], 'fss_schema_order_idx');
|
||||
$table->unique(['form_schema_id', 'slug'], 'fss_schema_slug_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_schema_sections');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_field_library', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('slug');
|
||||
$table->string('field_type', 50);
|
||||
$table->string('label');
|
||||
$table->text('help_text')->nullable();
|
||||
$table->json('options')->nullable();
|
||||
$table->json('validation_rules')->nullable();
|
||||
$table->boolean('default_is_required')->default(false);
|
||||
$table->boolean('default_is_filterable')->default(false);
|
||||
$table->json('default_binding')->nullable();
|
||||
$table->json('translations')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->unsignedInteger('usage_count')->default(0);
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['organisation_id', 'field_type'], 'ffl_org_type_idx');
|
||||
$table->index(['organisation_id', 'is_active'], 'ffl_org_active_idx');
|
||||
$table->unique(['organisation_id', 'slug'], 'ffl_org_slug_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_field_library');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_fields', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('form_schema_id')
|
||||
->constrained('form_schemas')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('form_schema_section_id')
|
||||
->nullable()
|
||||
->constrained('form_schema_sections')
|
||||
->nullOnDelete();
|
||||
$table->foreignUlid('library_field_id')
|
||||
->nullable()
|
||||
->constrained('form_field_library')
|
||||
->nullOnDelete();
|
||||
|
||||
// field_type is stored as string (not DB enum) to allow
|
||||
// runtime-registered custom types per ARCH §17.2.
|
||||
$table->string('field_type', 50);
|
||||
$table->string('slug', 100);
|
||||
$table->string('label');
|
||||
$table->text('help_text')->nullable();
|
||||
$table->string('section', 100)->nullable();
|
||||
$table->json('options')->nullable();
|
||||
$table->json('validation_rules')->nullable();
|
||||
$table->boolean('is_required')->default(false);
|
||||
$table->boolean('is_filterable')->default(false);
|
||||
$table->boolean('is_portal_visible')->default(true);
|
||||
$table->boolean('is_admin_only')->default(false);
|
||||
$table->boolean('is_unique')->default(false);
|
||||
$table->boolean('is_pii')->default(false);
|
||||
$table->string('display_width', 10)->default('full');
|
||||
$table->json('binding')->nullable();
|
||||
$table->json('conditional_logic')->nullable();
|
||||
$table->json('role_restrictions')->nullable();
|
||||
$table->json('translations')->nullable();
|
||||
$table->string('value_storage_hint', 10)->default('json');
|
||||
$table->boolean('review_required')->default(false);
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['form_schema_id', 'sort_order'], 'ff_schema_order_idx');
|
||||
$table->index(['form_schema_id', 'is_filterable'], 'ff_schema_filterable_idx');
|
||||
$table->index('library_field_id', 'ff_library_idx');
|
||||
// Uniqueness of slug-within-schema for non-deleted rows is
|
||||
// enforced at application level (FormFieldService); a composite
|
||||
// unique across (schema_id, slug) would collide on soft-delete
|
||||
// + re-create. Adjust if MySQL generated column approach preferred.
|
||||
$table->index(['form_schema_id', 'slug'], 'ff_schema_slug_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_fields');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_submissions', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('form_schema_id')
|
||||
->constrained('form_schemas')
|
||||
->cascadeOnDelete();
|
||||
|
||||
// Polymorphic subject
|
||||
$table->string('subject_type', 50)->nullable();
|
||||
$table->ulid('subject_id')->nullable();
|
||||
|
||||
$table->foreignUlid('submitted_by_user_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->string('public_submitter_name')->nullable();
|
||||
$table->string('public_submitter_email')->nullable();
|
||||
$table->string('public_submitter_ip', 45)->nullable();
|
||||
$table->timestamp('public_submitter_ip_anonymised_at')->nullable();
|
||||
|
||||
$table->string('status', 20);
|
||||
$table->string('review_status', 30)->nullable();
|
||||
$table->foreignUlid('reviewed_by_user_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->text('review_notes')->nullable();
|
||||
|
||||
$table->timestamp('submitted_at')->nullable();
|
||||
$table->unsignedInteger('schema_version_at_submit')->nullable();
|
||||
$table->json('schema_snapshot')->nullable();
|
||||
$table->boolean('is_test')->default(false);
|
||||
|
||||
$table->string('submitted_in_locale', 10)->nullable();
|
||||
$table->timestamp('opened_at')->nullable();
|
||||
$table->timestamp('first_interacted_at')->nullable();
|
||||
$table->unsignedInteger('submission_duration_seconds')->nullable();
|
||||
$table->unsignedInteger('auto_save_count')->default(0);
|
||||
|
||||
$table->ulid('idempotency_key')->nullable();
|
||||
$table->timestamp('anonymised_at')->nullable();
|
||||
|
||||
// MEDIUMTEXT for long concatenated submission content; FULLTEXT
|
||||
// added when engine supports it.
|
||||
$table->mediumText('search_index')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['form_schema_id', 'status'], 'fs_schema_status_idx');
|
||||
$table->index(['subject_type', 'subject_id'], 'fs_subject_idx');
|
||||
$table->index('submitted_by_user_id', 'fs_submitter_idx');
|
||||
$table->index(['form_schema_id', 'review_status'], 'fs_schema_review_idx');
|
||||
$table->index(['form_schema_id', 'idempotency_key'], 'fs_idempotency_idx');
|
||||
});
|
||||
|
||||
// FULLTEXT index: best-effort. InnoDB (MySQL 5.7+) supports it.
|
||||
// SQLite/older engines don't; we swallow the failure so tests that
|
||||
// run on SQLite can still migrate. Production MySQL gets the index.
|
||||
try {
|
||||
Schema::table('form_submissions', function (Blueprint $table): void {
|
||||
$table->fullText('search_index', 'fs_search_index_fulltext');
|
||||
});
|
||||
} catch (\Throwable) {
|
||||
// Engine does not support FULLTEXT — regular index lookups via
|
||||
// LIKE are the fallback. Documented in migration comment.
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_submissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_submission_section_statuses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignUlid('form_submission_id')
|
||||
->constrained('form_submissions')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('form_schema_section_id')
|
||||
->constrained('form_schema_sections')
|
||||
->cascadeOnDelete();
|
||||
$table->string('status', 30);
|
||||
$table->timestamp('submitted_at')->nullable();
|
||||
$table->foreignUlid('reviewed_by_user_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->text('review_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(
|
||||
['form_submission_id', 'form_schema_section_id'],
|
||||
'fsss_submission_section_unique'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_submission_section_statuses');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_submission_delegations', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('form_submission_id')
|
||||
->constrained('form_submissions')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('delegated_to_user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('delegated_by_user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->timestamp('granted_at');
|
||||
$table->timestamp('revoked_at')->nullable();
|
||||
$table->text('message')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['delegated_to_user_id', 'revoked_at'], 'fsd_delegatee_active_idx');
|
||||
$table->index('form_submission_id', 'fsd_submission_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_submission_delegations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_values', function (Blueprint $table) {
|
||||
// Integer AI PK: EAV table, joined heavily — int joins faster
|
||||
// than ULID joins (ARCH §4.4).
|
||||
$table->id();
|
||||
$table->foreignUlid('form_submission_id')
|
||||
->constrained('form_submissions')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('form_field_id')
|
||||
->constrained('form_fields')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->json('value');
|
||||
$table->string('value_indexed', 255)->nullable();
|
||||
$table->decimal('value_number', 15, 4)->nullable();
|
||||
$table->date('value_date')->nullable();
|
||||
$table->boolean('value_bool')->nullable();
|
||||
$table->boolean('value_anonymised')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(
|
||||
['form_submission_id', 'form_field_id'],
|
||||
'fv_submission_field_unique'
|
||||
);
|
||||
$table->index(['form_field_id', 'value_indexed'], 'fv_field_indexed_idx');
|
||||
$table->index(['form_field_id', 'value_number'], 'fv_field_number_idx');
|
||||
$table->index(['form_field_id', 'value_date'], 'fv_field_date_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_values');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_value_options', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('form_value_id')
|
||||
->constrained('form_values')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('form_field_id')
|
||||
->constrained('form_fields')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('form_submission_id')
|
||||
->constrained('form_submissions')
|
||||
->cascadeOnDelete();
|
||||
$table->string('option_value', 255);
|
||||
|
||||
$table->index(['form_field_id', 'option_value'], 'fvo_field_option_idx');
|
||||
$table->index('form_submission_id', 'fvo_submission_idx');
|
||||
$table->index('form_value_id', 'fvo_value_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_value_options');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_templates', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('slug');
|
||||
$table->string('purpose', 50);
|
||||
$table->text('description')->nullable();
|
||||
// schema_snapshot structure per ARCH §4.6.1
|
||||
$table->json('schema_snapshot');
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['organisation_id', 'purpose', 'is_active'], 'ft_org_purpose_active_idx');
|
||||
$table->unique(['organisation_id', 'slug'], 'ft_org_slug_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_schema_webhooks', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('form_schema_id')
|
||||
->constrained('form_schemas')
|
||||
->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('trigger_event', 40);
|
||||
// Encrypted storage: enforced by Eloquent cast on the model.
|
||||
// Column is TEXT to accommodate ciphertext length overhead.
|
||||
$table->text('url');
|
||||
$table->text('secret')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['form_schema_id', 'is_active'], 'fsw_schema_active_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_schema_webhooks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('form_webhook_deliveries', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('form_schema_webhook_id')
|
||||
->constrained('form_schema_webhooks')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignUlid('form_submission_id')
|
||||
->constrained('form_submissions')
|
||||
->cascadeOnDelete();
|
||||
$table->string('trigger_event', 40);
|
||||
$table->string('status', 20);
|
||||
$table->unsignedInteger('attempts')->default(0);
|
||||
$table->timestamp('last_attempt_at')->nullable();
|
||||
$table->unsignedSmallInteger('response_status')->nullable();
|
||||
$table->text('response_body_excerpt')->nullable();
|
||||
$table->timestamp('next_retry_at')->nullable();
|
||||
$table->timestamp('delivered_at')->nullable();
|
||||
$table->timestamp('failed_permanently_at')->nullable();
|
||||
$table->json('payload_snapshot');
|
||||
|
||||
$table->index(['status', 'next_retry_at'], 'fwd_status_retry_idx');
|
||||
$table->index(['form_schema_webhook_id', 'status'], 'fwd_webhook_status_idx');
|
||||
$table->index('form_submission_id', 'fwd_submission_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('form_webhook_deliveries');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user