feat(form-builder): denormalize organisation_id and event_id on form_submissions per addendum Q2
Adds direct tenant + event columns to form_submissions so rapportage-hot
aggregate queries (dashboards, CSV-exports, counts over thousands of rows
per org or per event) skip the form_schemas join. This is the single
denormalization exception per addendum Q2; every other form-builder child
table continues to resolve tenancy via FK-chain through its parent
(implemented in Commit 3).
Schema:
- form_submissions.organisation_id ULID FK → organisations, cascade delete, NOT NULL
- form_submissions.event_id ULID FK → events, null on delete, nullable
- Indexes: (organisation_id, status), (event_id, status)
Observer: App\Observers\FormBuilder\FormSubmissionObserver::creating
resolves both columns when the caller has not set them.
- organisation_id <- form_schema.organisation_id (always present —
form_schemas carries OrganisationScope's column directly)
- event_id <- schema.owner_id when owner_type === 'event'; else the
active route's {event} parameter; else null (user_profile /
signature_contract purposes)
The observer docblock spells out both resolution paths and is covered
by the observer test below.
Model: FormSubmission gains organisation_id + event_id in $fillable, a
belongsTo organisation() and belongsTo event() relation.
Factory: FormSubmissionFactory gains forOrganisation($org) and
forEvent($event) states for tests that need to override the observer's
automatic resolution (e.g. cross-org leakage scenarios in Commit 3).
Normal factory usage does not need the states — the observer populates
both fields on save.
Docs:
- SCHEMA.md §3.5.12 form_submissions table — organisation_id and event_id
inserted between form_schema_id and subject_type; indexes added;
addendum Q2 rationale paragraph at the bottom explaining why this is
the only denormalized form-builder child.
- ARCH-FORM-BUILDER.md §4.3 — mirror changes + rationale inline on the
columns and in the indexes list.
Tests: tests/Feature/FormBuilder/FormSubmissionObserverTest.php — 7 tests
covering organisation resolution from schema, event resolution from
event-owned schema, null event_id for non-event-owned schemas without
route context, route-based event resolution, organisation_id populated
on every create path (factory / new() / Model::create), index presence,
and belongsTo relations. 13 new assertions. Full suite: 984 passed
(2675 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
// Addendum Q2 exception: form_submissions is the single rapportage-hot
|
||||
// table that earns denormalized organisation_id + event_id columns.
|
||||
// Every other form-builder child table resolves its tenant via FK-chain
|
||||
// through its parent (see OrganisationScope).
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('form_submissions', function (Blueprint $table): void {
|
||||
// organisation_id is NOT NULL — every submission belongs to a
|
||||
// tenant. Pre-launch: form_submissions is empty at migration
|
||||
// time, so MySQL/SQLite accept NOT NULL without a default.
|
||||
$table->foreignUlid('organisation_id')
|
||||
->after('form_schema_id')
|
||||
->constrained('organisations')
|
||||
->cascadeOnDelete();
|
||||
|
||||
// event_id is nullable — purposes like user_profile have no
|
||||
// event. The observer resolves it from the schema owner
|
||||
// (owner_type === 'event') or from the active route.
|
||||
$table->foreignUlid('event_id')
|
||||
->nullable()
|
||||
->after('organisation_id')
|
||||
->constrained('events')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index(['organisation_id', 'status'], 'fs_org_status_idx');
|
||||
$table->index(['event_id', 'status'], 'fs_event_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('form_submissions', function (Blueprint $table): void {
|
||||
$table->dropIndex('fs_org_status_idx');
|
||||
$table->dropIndex('fs_event_status_idx');
|
||||
$table->dropConstrainedForeignId('organisation_id');
|
||||
$table->dropConstrainedForeignId('event_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user