`projectId` was nullable, and a null one made the row unreadable: every read
filters on that column, so an unattributed entry was written and then never
surfaced to anyone. The column is now NOT NULL, and `ActivityEventInput`
requires it.
The nullable column existed for one case, and that case turns out to be
fixable. `workflow-deleted` reaches a listener after the workflow row and its
`shared_workflow` rows have cascaded away, so the project cannot be looked up
at that point. But it can be captured before the delete: `workflow.service.ts`
already resolves the workflow, already calls `ownershipService`, and already
runs a `beforeWorkflowDeleted` hook whose stated purpose is capturing rows the
delete is about to cascade away. Enriching the event is upstream work, tracked
on the relay ticket.
Instance-level credentials are not an exception either. `instance_credential_-
assignment` maps a use slot to an ordinary credential that lives in a project;
what is instance-level is the assignment, not the credential.
Two test changes follow. The project-cascade test now writes to two projects
and asserts only the deleted one's entries go, since it can no longer lean on
an unattributed row surviving. And a new test confirms the column rejects an
entry with no project.
Master landed `CreateAgentBackgroundJobTable1788191436461` above this migration.
The `migration-timestamp` rule requires every added migration to sort after all
others, so the file moves to 1788253087010.
The rebase also touched the two generated schema READMEs. Both are regenerated
rather than merged by hand.
The `migration-timestamp` code-health rule rejects a PR that adds more than one
migration: every added file must sort above every other, including its own
sibling, so the lower of the two always violates. The rule's own test named
"accepts a multi-migration PR" asserts exactly that in its body. Rebasing also
put both timestamps below `CreateTypeAvailabilityPolicyTables1787841960965`,
which landed on master meanwhile, so both needed renaming regardless.
Collapsing to one file forced a choice the ticket left open, and the resource
index is the part that goes. The ticket's own design note says it "arrives with
the read that needs it, not before", and nothing reads it here — so the index,
its decorator, `findByResource` and their tests are all deferred to the reader.
That leaves the two indexes each justified by a foreign key as well as a read.
The create-table migration is unchanged apart from its timestamp; it never
created the resource index.
Append-only record of what recently happened on an instance: an
`ActivityEvent` entity, the `activity_event` table, and a repository with
the single write path plus the two retention deletes.
Nothing reads or writes the table yet, so this ships and rolls back
independently of any behaviour change.
It lives in core `@n8n/db` rather than a module: a record of what changed
on the instance is not specific to one consumer, and scoping it to one
would make every later reader either cross a module boundary or build a
second log.
Three decisions worth keeping:
- `resourceId` has no foreign key. An entry has to outlive what it
describes — a workflow deletion is the entry most worth keeping, and a
cascade erases it exactly when it becomes interesting.
- No CHECK on `category` or `action`. A feed covers more kinds with every
feature, and each widening is another SQLite table recreation. The
single writer is a typed relay, so the compiler already rejects a value
outside the union.
- Two indexes on creation, `(projectId, id)` and `(userId, id)`, each
justified by a read and a foreign key. The per-resource index lands in
its own migration.
Index assertions read the index DDL back rather than going through
`getTable()`, which returns index columns alphabetically on Postgres and
so would confirm a column order it never checked.