@substrat-run/kernel 0.138.0 → 0.140.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-log-ddl.d.ts +21 -0
- package/dist/admin-log-ddl.d.ts.map +1 -0
- package/dist/admin-log-ddl.js +32 -0
- package/dist/admin-log-ddl.js.map +1 -0
- package/dist/attachment-extractor.d.ts +21 -2
- package/dist/attachment-extractor.d.ts.map +1 -1
- package/dist/attachment-extractor.js +31 -6
- package/dist/attachment-extractor.js.map +1 -1
- package/dist/attachment-text.d.ts +64 -0
- package/dist/attachment-text.d.ts.map +1 -1
- package/dist/attachment-text.js +87 -16
- package/dist/attachment-text.js.map +1 -1
- package/dist/attribution.d.ts +36 -3
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +21 -8
- package/dist/attribution.js.map +1 -1
- package/dist/audit-outcome.d.ts +149 -0
- package/dist/audit-outcome.d.ts.map +1 -0
- package/dist/audit-outcome.js +233 -0
- package/dist/audit-outcome.js.map +1 -0
- package/dist/connect-links.d.ts +91 -0
- package/dist/connect-links.d.ts.map +1 -0
- package/dist/connect-links.js +189 -0
- package/dist/connect-links.js.map +1 -0
- package/dist/connect-state.d.ts +12 -3
- package/dist/connect-state.d.ts.map +1 -1
- package/dist/connect-state.js +8 -3
- package/dist/connect-state.js.map +1 -1
- package/dist/derived-object.d.ts +14 -0
- package/dist/derived-object.d.ts.map +1 -0
- package/dist/derived-object.js +2 -0
- package/dist/derived-object.js.map +1 -0
- package/dist/derived-objects.d.ts +108 -0
- package/dist/derived-objects.d.ts.map +1 -0
- package/dist/derived-objects.js +199 -0
- package/dist/derived-objects.js.map +1 -0
- package/dist/email-intent.d.ts +56 -0
- package/dist/email-intent.d.ts.map +1 -0
- package/dist/email-intent.js +71 -0
- package/dist/email-intent.js.map +1 -0
- package/dist/entity-edges.d.ts +6 -1
- package/dist/entity-edges.d.ts.map +1 -1
- package/dist/entity-edges.js +13 -0
- package/dist/entity-edges.js.map +1 -1
- package/dist/entity-grant-shape.d.ts +94 -0
- package/dist/entity-grant-shape.d.ts.map +1 -0
- package/dist/entity-grant-shape.js +329 -0
- package/dist/entity-grant-shape.js.map +1 -0
- package/dist/entity-grant.d.ts +45 -0
- package/dist/entity-grant.d.ts.map +1 -0
- package/dist/entity-grant.js +71 -0
- package/dist/entity-grant.js.map +1 -0
- package/dist/entity-state-reads.d.ts +40 -0
- package/dist/entity-state-reads.d.ts.map +1 -0
- package/dist/entity-state-reads.js +91 -0
- package/dist/entity-state-reads.js.map +1 -0
- package/dist/entity-state.d.ts +204 -0
- package/dist/entity-state.d.ts.map +1 -0
- package/dist/entity-state.js +356 -0
- package/dist/entity-state.js.map +1 -0
- package/dist/entity-trash.d.ts +245 -0
- package/dist/entity-trash.d.ts.map +1 -0
- package/dist/entity-trash.js +380 -0
- package/dist/entity-trash.js.map +1 -0
- package/dist/findings.d.ts +143 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +463 -0
- package/dist/findings.js.map +1 -0
- package/dist/grant-scoped-read.d.ts +41 -0
- package/dist/grant-scoped-read.d.ts.map +1 -0
- package/dist/grant-scoped-read.js +100 -0
- package/dist/grant-scoped-read.js.map +1 -0
- package/dist/index.d.ts +40 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/kernel-outbox.d.ts +14 -0
- package/dist/kernel-outbox.d.ts.map +1 -0
- package/dist/kernel-outbox.js +37 -0
- package/dist/kernel-outbox.js.map +1 -0
- package/dist/list-index.d.ts +34 -3
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +83 -31
- package/dist/list-index.js.map +1 -1
- package/dist/mail-sender.d.ts +78 -0
- package/dist/mail-sender.d.ts.map +1 -0
- package/dist/mail-sender.js +2 -0
- package/dist/mail-sender.js.map +1 -0
- package/dist/membership-executor.d.ts +7 -47
- package/dist/membership-executor.d.ts.map +1 -1
- package/dist/membership-executor.js +28 -53
- package/dist/membership-executor.js.map +1 -1
- package/dist/migration-digest.d.ts +161 -0
- package/dist/migration-digest.d.ts.map +1 -0
- package/dist/migration-digest.js +237 -0
- package/dist/migration-digest.js.map +1 -0
- package/dist/module-erasure.d.ts +156 -0
- package/dist/module-erasure.d.ts.map +1 -0
- package/dist/module-erasure.js +440 -0
- package/dist/module-erasure.js.map +1 -0
- package/dist/module-migrations.d.ts +6 -3
- package/dist/module-migrations.d.ts.map +1 -1
- package/dist/module-migrations.js +12 -5
- package/dist/module-migrations.js.map +1 -1
- package/dist/peer.d.ts +9 -1
- package/dist/peer.d.ts.map +1 -1
- package/dist/peer.js +29 -0
- package/dist/peer.js.map +1 -1
- package/dist/permission-checker.d.ts +6 -0
- package/dist/permission-checker.d.ts.map +1 -1
- package/dist/permission-checker.js.map +1 -1
- package/dist/permission-eval.d.ts +51 -1
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +104 -2
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-sweep.d.ts +8 -2
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +17 -0
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-host.d.ts +479 -24
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +82 -7
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-role-admin.d.ts +68 -0
- package/dist/scope-role-admin.d.ts.map +1 -0
- package/dist/scope-role-admin.js +80 -0
- package/dist/scope-role-admin.js.map +1 -0
- package/dist/search-index.d.ts +21 -1
- package/dist/search-index.d.ts.map +1 -1
- package/dist/search-index.js +60 -37
- package/dist/search-index.js.map +1 -1
- package/dist/spine-guard.d.ts +71 -2
- package/dist/spine-guard.d.ts.map +1 -1
- package/dist/spine-guard.js +393 -10
- package/dist/spine-guard.js.map +1 -1
- package/dist/spine-restore.d.ts.map +1 -1
- package/dist/spine-restore.js +5 -0
- package/dist/spine-restore.js.map +1 -1
- package/dist/sql-identifier.d.ts +12 -0
- package/dist/sql-identifier.d.ts.map +1 -0
- package/dist/sql-identifier.js +17 -0
- package/dist/sql-identifier.js.map +1 -0
- package/dist/sql-statements.d.ts +71 -0
- package/dist/sql-statements.d.ts.map +1 -0
- package/dist/sql-statements.js +191 -0
- package/dist/sql-statements.js.map +1 -0
- package/dist/subject-redaction.d.ts +9 -1
- package/dist/subject-redaction.d.ts.map +1 -1
- package/dist/subject-redaction.js.map +1 -1
- package/dist/system-switch-record.d.ts +3 -0
- package/dist/system-switch-record.d.ts.map +1 -1
- package/dist/system-switch-record.js +2 -0
- package/dist/system-switch-record.js.map +1 -1
- package/dist/table-ownership.d.ts +103 -0
- package/dist/table-ownership.d.ts.map +1 -0
- package/dist/table-ownership.js +285 -0
- package/dist/table-ownership.js.map +1 -0
- package/dist/timeline.d.ts +4 -4
- package/dist/timeline.js +4 -4
- package/dist/visible-continuation.d.ts +38 -0
- package/dist/visible-continuation.d.ts.map +1 -0
- package/dist/visible-continuation.js +162 -0
- package/dist/visible-continuation.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity archive and trash (#119): the kernel half, written once — both adapters hand
|
|
3
|
+
* `createEntityStateVerbs` the same things, the way `createEntityEdgeVerbs` is shared.
|
|
4
|
+
*
|
|
5
|
+
* ## What the kernel owns, and what it leaves to the vertical
|
|
6
|
+
*
|
|
7
|
+
* | Kernel | Vertical |
|
|
8
|
+
* |---|---|
|
|
9
|
+
* | The two columns on the entity's table, added by a derived migration | Which entities declare `archive` / `trash`, and the keys |
|
|
10
|
+
* | The transitions, their refusals, and the event each one emits | The operations that call the verbs, and their routes |
|
|
11
|
+
* | The declared key, re-checked on the entity by every verb and every trashed read | What an archived row looks like on a screen |
|
|
12
|
+
* | `ctx.page` / `ctx.search` leaving archived and trashed rows out by default | Its own hand-written `SELECT`s — see the gap below |
|
|
13
|
+
*
|
|
14
|
+
* ## The state is two columns
|
|
15
|
+
*
|
|
16
|
+
* `_substrat_archived_at` and `_substrat_trashed_at`, each an ISO instant or NULL. Apart, not
|
|
17
|
+
* one enum, so a trash never forgets an archive: `restore` clears the trash and leaves the
|
|
18
|
+
* archive, and an archived row that went to the bin comes back archived. The visible state is
|
|
19
|
+
* derived (contracts `entity-state.ts`): trashed, else archived, else active.
|
|
20
|
+
*
|
|
21
|
+
* Module code cannot write either column: `ctx.sql` refuses a write naming a `_substrat_*`
|
|
22
|
+
* column (`spine-guard.ts`), so the only way into or out of the trash is a verb that checks
|
|
23
|
+
* the key and emits the event. It can READ them, which is how a vertical's own `SELECT` keeps
|
|
24
|
+
* archived rows out of a screen `ctx.page` does not compose.
|
|
25
|
+
*
|
|
26
|
+
* ## The gap, stated
|
|
27
|
+
*
|
|
28
|
+
* The kernel filters the reads it composes — `ctx.page` and `ctx.search` — and nothing else.
|
|
29
|
+
* A handler's own `SELECT … FROM todo_lists WHERE id = ?` sees an archived or trashed row
|
|
30
|
+
* exactly as before. That is the honest limit of a kernel that does not parse module SQL:
|
|
31
|
+
* a get-by-id asks `ctx.entityState(ref)` and decides, and a hand-written list adds the
|
|
32
|
+
* predicate itself (`entityStateWhere`).
|
|
33
|
+
*
|
|
34
|
+
* ## Trash is not erasure, and not tenant deletion
|
|
35
|
+
*
|
|
36
|
+
* Trash keeps the row and every event about it. Subject erasure (#37) reaches a trashed row
|
|
37
|
+
* exactly as it reaches any other, because the row never moved tables; a tenant's deletion
|
|
38
|
+
* (#36) removes it with everything else. Nothing here extends a retention or shortens one.
|
|
39
|
+
*/
|
|
40
|
+
import { type DomainEventInput, type EntityStateDeclaration, type EntityStateName, type Instant, type PermissionKey } from '@substrat-run/contracts';
|
|
41
|
+
import type { OperationContext, ScopedSql, SqlMigration } from './scope-host.js';
|
|
42
|
+
import type { DerivedObject } from './derived-object.js';
|
|
43
|
+
/** A resolved declaration: everything the DDL, the reads and the verbs need. */
|
|
44
|
+
export interface EntityStatePlan {
|
|
45
|
+
readonly moduleId: string;
|
|
46
|
+
readonly entityType: string;
|
|
47
|
+
readonly table: string;
|
|
48
|
+
readonly idColumn: string;
|
|
49
|
+
/** Present when the entity can be archived. */
|
|
50
|
+
readonly archivePermission?: PermissionKey;
|
|
51
|
+
/** Present when the entity can be trashed. */
|
|
52
|
+
readonly trashPermission?: PermissionKey;
|
|
53
|
+
/** Present when a trashed entity is purged after this many days (`entity-trash.ts`). */
|
|
54
|
+
readonly purgeAfterDays?: number;
|
|
55
|
+
}
|
|
56
|
+
/** The operation's own check — a pass is recorded as one of its authorizations (K-34). */
|
|
57
|
+
export type StateCheck = OperationContext['check'];
|
|
58
|
+
/**
|
|
59
|
+
* Resolve one module's declarations. Refuses rather than skips, for `searchIndexPlans`'s
|
|
60
|
+
* reason: a declaration the author believes is live that silently is not leaves a trashed
|
|
61
|
+
* row in every list with no error anywhere.
|
|
62
|
+
*/
|
|
63
|
+
export declare function entityStatePlans(moduleId: string, declarations: readonly EntityStateDeclaration[] | undefined): EntityStatePlan[];
|
|
64
|
+
/**
|
|
65
|
+
* The migrations that add the columns — one per column, so declaring `trash` on an entity
|
|
66
|
+
* that already declared `archive` adds one column and re-runs nothing.
|
|
67
|
+
*
|
|
68
|
+
* **The version is the declaration**, as for search and list indexes. Unlike theirs, this DDL
|
|
69
|
+
* is not drop-then-create: a column holds the state, and dropping it would un-trash every row.
|
|
70
|
+
* So it runs once per column, ever. Withdrawing a declaration leaves the column where it is,
|
|
71
|
+
* holding what it held; re-declaring it finds the migration already applied.
|
|
72
|
+
*
|
|
73
|
+
* Applied after the module's own migrations (the table must exist) and before its list
|
|
74
|
+
* indexes (their partial `WHERE` names these columns) — `moduleMigrations` writes that order.
|
|
75
|
+
*/
|
|
76
|
+
export declare function entityStateMigrations(moduleId: string, declarations: readonly EntityStateDeclaration[] | undefined): SqlMigration[];
|
|
77
|
+
/** The column migrations' versions, and the column each adds — what the journal says a table holds. */
|
|
78
|
+
export declare const stateColumnVersionsOf: (plan: EntityStatePlan) => {
|
|
79
|
+
version: string;
|
|
80
|
+
column: string;
|
|
81
|
+
}[];
|
|
82
|
+
/** The purge index's migration version (#119) — present only on a plan with a purge horizon. */
|
|
83
|
+
export declare const purgeIndexVersion: (plan: EntityStatePlan) => string;
|
|
84
|
+
/**
|
|
85
|
+
* The partial index the purge sweep walks (#119): trashed rows only, oldest trash first, so a
|
|
86
|
+
* sweep reads the due rows and nothing else. Its name is kernel-prefixed, so the reserved
|
|
87
|
+
* namespace. Empty for a plan without a purge horizon.
|
|
88
|
+
*/
|
|
89
|
+
export declare function purgeIndexObjects(plan: EntityStatePlan): DerivedObject[];
|
|
90
|
+
/**
|
|
91
|
+
* The purge index's DDL. `IF NOT EXISTS` because a dump load runs it again after the rows are
|
|
92
|
+
* in; a repair that finds it wrong drops it first (`purgeIndexRepairDdl`).
|
|
93
|
+
*/
|
|
94
|
+
export declare function purgeIndexDdl(plan: EntityStatePlan): string;
|
|
95
|
+
/** Drop-then-create, for a purge index the catalogue holds under its name but not as emitted. */
|
|
96
|
+
export declare function purgeIndexRepairDdl(plan: EntityStatePlan): string;
|
|
97
|
+
/**
|
|
98
|
+
* The guard triggers' version. By which columns they guard, so declaring a trash on an
|
|
99
|
+
* archivable entity rebuilds them to guard both.
|
|
100
|
+
*/
|
|
101
|
+
export declare const entityStateGuardVersion: (plan: EntityStatePlan) => string;
|
|
102
|
+
/** The prefix of every trigger this module derives — kernel-owned, so the reserved one. */
|
|
103
|
+
export declare const ENTITY_STATE_TRIGGER_PREFIX = "_substrat_state_";
|
|
104
|
+
/**
|
|
105
|
+
* The kernel's authorization for ONE move (#119, Codex r2): the row `ctx.archive` & co. write
|
|
106
|
+
* just before their `UPDATE` and delete just after it, in the same transaction. The update
|
|
107
|
+
* trigger below refuses any change to the columns that has no such row — so the only writer the
|
|
108
|
+
* columns admit is the one that can write a `_substrat_*` TABLE, which `ctx.sql` refuses by name
|
|
109
|
+
* (a far simpler reading than finding an assignment target in an expression).
|
|
110
|
+
*
|
|
111
|
+
* Never holds a row between operations: written and removed inside one move, and a move that
|
|
112
|
+
* throws rolls back with its operation. Shared by both adapters' `KERNEL_DDL`, like the other
|
|
113
|
+
* kernel-owned spine tables, so the two cannot part company.
|
|
114
|
+
*/
|
|
115
|
+
export declare const ENTITY_STATE_MOVES_TABLE = "_substrat_state_moves";
|
|
116
|
+
export declare const ENTITY_STATE_MOVES_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_state_moves (\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n PRIMARY KEY (entity_type, entity_id)\n );\n";
|
|
117
|
+
/**
|
|
118
|
+
* The invariant below the column guard, as two triggers on the entity's table.
|
|
119
|
+
*
|
|
120
|
+
* `ctx.sql` refuses the writes that would set the columns (`assertNoReservedColumnWrite`), but
|
|
121
|
+
* that is a reading of SQL text, and a reading can miss a form — it did, twice. These hold
|
|
122
|
+
* whatever the text says:
|
|
123
|
+
*
|
|
124
|
+
* - **born** — `BEFORE INSERT`: a row is never inserted archived or trashed. Every row enters
|
|
125
|
+
* active.
|
|
126
|
+
* - **moved** — `BEFORE UPDATE OF` the columns: a change to either aborts unless the kernel's
|
|
127
|
+
* authorization row for this entity exists (`ENTITY_STATE_MOVES_TABLE`), which only a move
|
|
128
|
+
* writes. An `UPDATE` that does not name the columns does not fire it.
|
|
129
|
+
*
|
|
130
|
+
* Drop-then-create, like the search triggers, and re-run after a dump load: a load drops the
|
|
131
|
+
* table and with it the triggers, then inserts the rows first — a restored binned row is
|
|
132
|
+
* legitimately born trashed, so the triggers are put back only after them.
|
|
133
|
+
*/
|
|
134
|
+
export declare function entityStateTriggerDdl(plan: EntityStatePlan): string;
|
|
135
|
+
/** The two guard triggers `entityStateTriggerDdl` creates, each with its CREATE statement. */
|
|
136
|
+
export declare function entityStateTriggerObjects(plan: EntityStatePlan): DerivedObject[];
|
|
137
|
+
/**
|
|
138
|
+
* Add one module's plans to a scope's registry, refusing a second declaration of a type — the
|
|
139
|
+
* table is one module's, and two answers to "who may trash it" is no answer — and a key the
|
|
140
|
+
* module does not declare. The entity is the module's, so its keys are too: a key nobody
|
|
141
|
+
* declared reaches no role, no grant shape and no `PERMISSIONS.md`, and would make the verb
|
|
142
|
+
* look gated while nobody could ever pass it.
|
|
143
|
+
*/
|
|
144
|
+
export declare function addStatePlans(byType: Map<string, EntityStatePlan>, moduleId: string, declarations: readonly EntityStateDeclaration[] | undefined, declaredKeys: readonly {
|
|
145
|
+
readonly key: string;
|
|
146
|
+
}[]): void;
|
|
147
|
+
/** The tables whose rows carry the columns, lowercased as SQLite resolves a name — `guardSpine`'s input. */
|
|
148
|
+
export declare const statefulTablesOf: (plans: ReadonlyMap<string, EntityStatePlan>) => ReadonlySet<string>;
|
|
149
|
+
/** The columns a plan has, for code that only needs to know which views exist. */
|
|
150
|
+
export interface StateColumns {
|
|
151
|
+
readonly archive: boolean;
|
|
152
|
+
readonly trash: boolean;
|
|
153
|
+
}
|
|
154
|
+
export declare const stateColumnsOf: (plan: EntityStatePlan) => StateColumns;
|
|
155
|
+
/** The views an entity with these columns has — `active` always, then one per column. */
|
|
156
|
+
export declare function viewsOf(columns: StateColumns): EntityStateName[];
|
|
157
|
+
/**
|
|
158
|
+
* The predicate selecting one view's rows, over bare column names or `alias.`-qualified ones.
|
|
159
|
+
*
|
|
160
|
+
* **Spelled one way, everywhere.** A partial index is used only when the query's `WHERE`
|
|
161
|
+
* contains the index's own terms, so the list index DDL and the list query both come from
|
|
162
|
+
* here — a reworded copy would plan a scan in production and pass every test.
|
|
163
|
+
*
|
|
164
|
+
* `columns` absent is an entity that declares no state: every row is active, so there is no
|
|
165
|
+
* predicate. A view the entity does not have is refused rather than answered empty — asking
|
|
166
|
+
* an entity with no trash for its trashed rows is a wiring mistake, and an empty page would
|
|
167
|
+
* read as an empty bin.
|
|
168
|
+
*/
|
|
169
|
+
export declare function entityStateWhere(entityType: string, columns: StateColumns, view: EntityStateName, alias?: string): string;
|
|
170
|
+
export declare function entityStateWhere(entityType: string, columns: StateColumns | undefined, view: EntityStateName | undefined, alias?: string): string | undefined;
|
|
171
|
+
/** The two columns as one row holds them. */
|
|
172
|
+
export interface StateRow {
|
|
173
|
+
readonly archived_at: string | null;
|
|
174
|
+
readonly trashed_at: string | null;
|
|
175
|
+
}
|
|
176
|
+
/** One row's two columns, or `undefined` when the row does not exist. */
|
|
177
|
+
export declare function readStateRow(sql: ScopedSql, plan: EntityStatePlan, entityId: string): StateRow | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* The plan for `entityType`, and the key one of its states is gated on — or `validation_failed`
|
|
180
|
+
* naming what the entity does not declare. Shared by the verbs and the trashed readers, so
|
|
181
|
+
* "declares no trash" is one sentence.
|
|
182
|
+
*/
|
|
183
|
+
export declare function stateKeyOf(plans: ReadonlyMap<string, EntityStatePlan>, verb: string, entityType: string, which: 'archive' | 'trash'): {
|
|
184
|
+
plan: EntityStatePlan;
|
|
185
|
+
key: PermissionKey;
|
|
186
|
+
};
|
|
187
|
+
/** What the verbs need from the adapter. */
|
|
188
|
+
export interface EntityStateDeps {
|
|
189
|
+
/** RAW access inside the operation's own transaction — the guarded `ctx.sql` refuses these columns. */
|
|
190
|
+
sql: ScopedSql;
|
|
191
|
+
/** entity type → its plan, for every registered module. */
|
|
192
|
+
plans: ReadonlyMap<string, EntityStatePlan>;
|
|
193
|
+
/** The operation's instant — what each column is stamped with. */
|
|
194
|
+
now: Instant;
|
|
195
|
+
check: StateCheck;
|
|
196
|
+
/** `ctx.emit`'s kernel writer — stamps the actor, the authorization chain and the operation. */
|
|
197
|
+
emit: (event: DomainEventInput) => void;
|
|
198
|
+
/** K-42's read-only refusal, for the effecting verbs. */
|
|
199
|
+
assertWrites: (verb: string) => void;
|
|
200
|
+
}
|
|
201
|
+
/** The verbs, as `OperationContext` carries them. */
|
|
202
|
+
export type EntityStateVerbs = Pick<OperationContext, 'archive' | 'unarchive' | 'trash' | 'restore' | 'entityState'>;
|
|
203
|
+
export declare function createEntityStateVerbs(deps: EntityStateDeps): EntityStateVerbs;
|
|
204
|
+
//# sourceMappingURL=entity-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-state.d.ts","sourceRoot":"","sources":["../src/entity-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity archive and trash (#119): the kernel half, written once — both adapters hand
|
|
3
|
+
* `createEntityStateVerbs` the same things, the way `createEntityEdgeVerbs` is shared.
|
|
4
|
+
*
|
|
5
|
+
* ## What the kernel owns, and what it leaves to the vertical
|
|
6
|
+
*
|
|
7
|
+
* | Kernel | Vertical |
|
|
8
|
+
* |---|---|
|
|
9
|
+
* | The two columns on the entity's table, added by a derived migration | Which entities declare `archive` / `trash`, and the keys |
|
|
10
|
+
* | The transitions, their refusals, and the event each one emits | The operations that call the verbs, and their routes |
|
|
11
|
+
* | The declared key, re-checked on the entity by every verb and every trashed read | What an archived row looks like on a screen |
|
|
12
|
+
* | `ctx.page` / `ctx.search` leaving archived and trashed rows out by default | Its own hand-written `SELECT`s — see the gap below |
|
|
13
|
+
*
|
|
14
|
+
* ## The state is two columns
|
|
15
|
+
*
|
|
16
|
+
* `_substrat_archived_at` and `_substrat_trashed_at`, each an ISO instant or NULL. Apart, not
|
|
17
|
+
* one enum, so a trash never forgets an archive: `restore` clears the trash and leaves the
|
|
18
|
+
* archive, and an archived row that went to the bin comes back archived. The visible state is
|
|
19
|
+
* derived (contracts `entity-state.ts`): trashed, else archived, else active.
|
|
20
|
+
*
|
|
21
|
+
* Module code cannot write either column: `ctx.sql` refuses a write naming a `_substrat_*`
|
|
22
|
+
* column (`spine-guard.ts`), so the only way into or out of the trash is a verb that checks
|
|
23
|
+
* the key and emits the event. It can READ them, which is how a vertical's own `SELECT` keeps
|
|
24
|
+
* archived rows out of a screen `ctx.page` does not compose.
|
|
25
|
+
*
|
|
26
|
+
* ## The gap, stated
|
|
27
|
+
*
|
|
28
|
+
* The kernel filters the reads it composes — `ctx.page` and `ctx.search` — and nothing else.
|
|
29
|
+
* A handler's own `SELECT … FROM todo_lists WHERE id = ?` sees an archived or trashed row
|
|
30
|
+
* exactly as before. That is the honest limit of a kernel that does not parse module SQL:
|
|
31
|
+
* a get-by-id asks `ctx.entityState(ref)` and decides, and a hand-written list adds the
|
|
32
|
+
* predicate itself (`entityStateWhere`).
|
|
33
|
+
*
|
|
34
|
+
* ## Trash is not erasure, and not tenant deletion
|
|
35
|
+
*
|
|
36
|
+
* Trash keeps the row and every event about it. Subject erasure (#37) reaches a trashed row
|
|
37
|
+
* exactly as it reaches any other, because the row never moved tables; a tenant's deletion
|
|
38
|
+
* (#36) removes it with everything else. Nothing here extends a retention or shortens one.
|
|
39
|
+
*/
|
|
40
|
+
import { ARCHIVED_AT_COLUMN, ENTITY_ARCHIVED, ENTITY_RESTORED, ENTITY_TRASHED, ENTITY_UNARCHIVED, TRASHED_AT_COLUMN, entityStateChangedPayload, substratError, } from '@substrat-run/contracts';
|
|
41
|
+
import { assertAllowed } from './permission-checker.js';
|
|
42
|
+
import { assertSqlIdentifier } from './sql-identifier.js';
|
|
43
|
+
/**
|
|
44
|
+
* Resolve one module's declarations. Refuses rather than skips, for `searchIndexPlans`'s
|
|
45
|
+
* reason: a declaration the author believes is live that silently is not leaves a trashed
|
|
46
|
+
* row in every list with no error anywhere.
|
|
47
|
+
*/
|
|
48
|
+
export function entityStatePlans(moduleId, declarations) {
|
|
49
|
+
if (!declarations?.length)
|
|
50
|
+
return [];
|
|
51
|
+
const plans = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const decl of declarations) {
|
|
54
|
+
const where = `${moduleId} entityStates['${decl.entityType}']`;
|
|
55
|
+
if (seen.has(decl.entityType))
|
|
56
|
+
throw new Error(`entity state: ${where} is declared twice`);
|
|
57
|
+
seen.add(decl.entityType);
|
|
58
|
+
if (!decl.table) {
|
|
59
|
+
throw new Error(`entity state: ${where} carries no table — declare \`archive\`/\`trash\` on the entity and ` +
|
|
60
|
+
'spread `manifestEntities()`, so the entity registry supplies it');
|
|
61
|
+
}
|
|
62
|
+
if (!decl.archivePermission && !decl.trashPermission) {
|
|
63
|
+
throw new Error(`entity state: ${where} declares neither an archive nor a trash permission`);
|
|
64
|
+
}
|
|
65
|
+
if (decl.purgeAfterDays !== undefined && !decl.trashPermission) {
|
|
66
|
+
throw new Error(`entity state: ${where} declares a purge horizon and no trash — only a trashed entity is purged`);
|
|
67
|
+
}
|
|
68
|
+
plans.push({
|
|
69
|
+
moduleId,
|
|
70
|
+
entityType: decl.entityType,
|
|
71
|
+
table: assertSqlIdentifier('entity state', 'a table', decl.table, where),
|
|
72
|
+
idColumn: assertSqlIdentifier('entity state', 'an id column', decl.idColumn ?? 'id', where),
|
|
73
|
+
...(decl.archivePermission ? { archivePermission: decl.archivePermission } : {}),
|
|
74
|
+
...(decl.trashPermission ? { trashPermission: decl.trashPermission } : {}),
|
|
75
|
+
...(decl.purgeAfterDays !== undefined ? { purgeAfterDays: decl.purgeAfterDays } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return plans;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The migrations that add the columns — one per column, so declaring `trash` on an entity
|
|
82
|
+
* that already declared `archive` adds one column and re-runs nothing.
|
|
83
|
+
*
|
|
84
|
+
* **The version is the declaration**, as for search and list indexes. Unlike theirs, this DDL
|
|
85
|
+
* is not drop-then-create: a column holds the state, and dropping it would un-trash every row.
|
|
86
|
+
* So it runs once per column, ever. Withdrawing a declaration leaves the column where it is,
|
|
87
|
+
* holding what it held; re-declaring it finds the migration already applied.
|
|
88
|
+
*
|
|
89
|
+
* Applied after the module's own migrations (the table must exist) and before its list
|
|
90
|
+
* indexes (their partial `WHERE` names these columns) — `moduleMigrations` writes that order.
|
|
91
|
+
*/
|
|
92
|
+
export function entityStateMigrations(moduleId, declarations) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const plan of entityStatePlans(moduleId, declarations)) {
|
|
95
|
+
for (const { version, column } of stateColumnVersionsOf(plan)) {
|
|
96
|
+
out.push({ version, sql: `ALTER TABLE ${plan.table} ADD COLUMN ${column} TEXT;` });
|
|
97
|
+
}
|
|
98
|
+
// After the columns they name.
|
|
99
|
+
out.push({ version: entityStateGuardVersion(plan), sql: entityStateTriggerDdl(plan) });
|
|
100
|
+
// The purge sweep's walk (#119): the oldest trashed rows first, without reading the rest of
|
|
101
|
+
// the bin. Its own version, so declaring a horizon later adds it and re-runs nothing.
|
|
102
|
+
if (plan.purgeAfterDays !== undefined)
|
|
103
|
+
out.push({ version: purgeIndexVersion(plan), sql: purgeIndexDdl(plan) });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** The column migrations' versions, and the column each adds — what the journal says a table holds. */
|
|
108
|
+
export const stateColumnVersionsOf = (plan) => [
|
|
109
|
+
...(plan.archivePermission ? [{ version: `state/${plan.entityType}:archive`, column: ARCHIVED_AT_COLUMN }] : []),
|
|
110
|
+
...(plan.trashPermission ? [{ version: `state/${plan.entityType}:trash`, column: TRASHED_AT_COLUMN }] : []),
|
|
111
|
+
];
|
|
112
|
+
/** The purge index's migration version (#119) — present only on a plan with a purge horizon. */
|
|
113
|
+
export const purgeIndexVersion = (plan) => `state/${plan.entityType}:purge`;
|
|
114
|
+
/**
|
|
115
|
+
* The partial index the purge sweep walks (#119): trashed rows only, oldest trash first, so a
|
|
116
|
+
* sweep reads the due rows and nothing else. Its name is kernel-prefixed, so the reserved
|
|
117
|
+
* namespace. Empty for a plan without a purge horizon.
|
|
118
|
+
*/
|
|
119
|
+
export function purgeIndexObjects(plan) {
|
|
120
|
+
if (plan.purgeAfterDays === undefined)
|
|
121
|
+
return [];
|
|
122
|
+
const name = `_substrat_purge_${plan.table}`;
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
name,
|
|
126
|
+
type: 'index',
|
|
127
|
+
table: plan.table,
|
|
128
|
+
sql: `CREATE INDEX ${name} ON ${plan.table} (${TRASHED_AT_COLUMN}, ${plan.idColumn}) WHERE ${TRASHED_AT_COLUMN} IS NOT NULL`,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The purge index's DDL. `IF NOT EXISTS` because a dump load runs it again after the rows are
|
|
134
|
+
* in; a repair that finds it wrong drops it first (`purgeIndexRepairDdl`).
|
|
135
|
+
*/
|
|
136
|
+
export function purgeIndexDdl(plan) {
|
|
137
|
+
return purgeIndexObjects(plan)
|
|
138
|
+
.map((idx) => `${idx.sql.replace(/^CREATE INDEX /, 'CREATE INDEX IF NOT EXISTS ')};`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
}
|
|
141
|
+
/** Drop-then-create, for a purge index the catalogue holds under its name but not as emitted. */
|
|
142
|
+
export function purgeIndexRepairDdl(plan) {
|
|
143
|
+
return purgeIndexObjects(plan)
|
|
144
|
+
.flatMap((idx) => [`DROP INDEX IF EXISTS ${idx.name};`, `${idx.sql};`])
|
|
145
|
+
.join('\n');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The guard triggers' version. By which columns they guard, so declaring a trash on an
|
|
149
|
+
* archivable entity rebuilds them to guard both.
|
|
150
|
+
*/
|
|
151
|
+
export const entityStateGuardVersion = (plan) => {
|
|
152
|
+
const { archive, trash } = stateColumnsOf(plan);
|
|
153
|
+
return `state/${plan.entityType}:guard:${[...(archive ? ['archive'] : []), ...(trash ? ['trash'] : [])].join('+')}`;
|
|
154
|
+
};
|
|
155
|
+
/** The prefix of every trigger this module derives — kernel-owned, so the reserved one. */
|
|
156
|
+
export const ENTITY_STATE_TRIGGER_PREFIX = '_substrat_state_';
|
|
157
|
+
/**
|
|
158
|
+
* The kernel's authorization for ONE move (#119, Codex r2): the row `ctx.archive` & co. write
|
|
159
|
+
* just before their `UPDATE` and delete just after it, in the same transaction. The update
|
|
160
|
+
* trigger below refuses any change to the columns that has no such row — so the only writer the
|
|
161
|
+
* columns admit is the one that can write a `_substrat_*` TABLE, which `ctx.sql` refuses by name
|
|
162
|
+
* (a far simpler reading than finding an assignment target in an expression).
|
|
163
|
+
*
|
|
164
|
+
* Never holds a row between operations: written and removed inside one move, and a move that
|
|
165
|
+
* throws rolls back with its operation. Shared by both adapters' `KERNEL_DDL`, like the other
|
|
166
|
+
* kernel-owned spine tables, so the two cannot part company.
|
|
167
|
+
*/
|
|
168
|
+
export const ENTITY_STATE_MOVES_TABLE = '_substrat_state_moves';
|
|
169
|
+
export const ENTITY_STATE_MOVES_DDL = `
|
|
170
|
+
CREATE TABLE IF NOT EXISTS _substrat_state_moves (
|
|
171
|
+
entity_type TEXT NOT NULL,
|
|
172
|
+
entity_id TEXT NOT NULL,
|
|
173
|
+
PRIMARY KEY (entity_type, entity_id)
|
|
174
|
+
);
|
|
175
|
+
`;
|
|
176
|
+
/** A string literal for SQL text: the entity type is a declaration, still quoted rather than trusted. */
|
|
177
|
+
const literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
178
|
+
/**
|
|
179
|
+
* The invariant below the column guard, as two triggers on the entity's table.
|
|
180
|
+
*
|
|
181
|
+
* `ctx.sql` refuses the writes that would set the columns (`assertNoReservedColumnWrite`), but
|
|
182
|
+
* that is a reading of SQL text, and a reading can miss a form — it did, twice. These hold
|
|
183
|
+
* whatever the text says:
|
|
184
|
+
*
|
|
185
|
+
* - **born** — `BEFORE INSERT`: a row is never inserted archived or trashed. Every row enters
|
|
186
|
+
* active.
|
|
187
|
+
* - **moved** — `BEFORE UPDATE OF` the columns: a change to either aborts unless the kernel's
|
|
188
|
+
* authorization row for this entity exists (`ENTITY_STATE_MOVES_TABLE`), which only a move
|
|
189
|
+
* writes. An `UPDATE` that does not name the columns does not fire it.
|
|
190
|
+
*
|
|
191
|
+
* Drop-then-create, like the search triggers, and re-run after a dump load: a load drops the
|
|
192
|
+
* table and with it the triggers, then inserts the rows first — a restored binned row is
|
|
193
|
+
* legitimately born trashed, so the triggers are put back only after them.
|
|
194
|
+
*/
|
|
195
|
+
export function entityStateTriggerDdl(plan) {
|
|
196
|
+
return entityStateTriggerObjects(plan)
|
|
197
|
+
.flatMap((t) => [`DROP TRIGGER IF EXISTS ${t.name};`, `${t.sql};`])
|
|
198
|
+
.join('\n');
|
|
199
|
+
}
|
|
200
|
+
/** The two guard triggers `entityStateTriggerDdl` creates, each with its CREATE statement. */
|
|
201
|
+
export function entityStateTriggerObjects(plan) {
|
|
202
|
+
const born = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_born`;
|
|
203
|
+
const moved = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_moved`;
|
|
204
|
+
const columns = stateColumnVersionsOf(plan).map((c) => c.column);
|
|
205
|
+
const trigger = (name, lines) => ({ name, type: 'trigger', table: plan.table, sql: lines.join('\n') });
|
|
206
|
+
return [
|
|
207
|
+
trigger(born, [
|
|
208
|
+
`CREATE TRIGGER ${born} BEFORE INSERT ON ${plan.table} WHEN ${columns.map((c) => `NEW.${c} IS NOT NULL`).join(' OR ')} BEGIN`,
|
|
209
|
+
` SELECT RAISE(ABORT, 'a row is never inserted archived or trashed - ctx.archive and ctx.trash move it (#119)');`,
|
|
210
|
+
`END`,
|
|
211
|
+
]),
|
|
212
|
+
trigger(moved, [
|
|
213
|
+
`CREATE TRIGGER ${moved} BEFORE UPDATE OF ${columns.join(', ')} ON ${plan.table}`,
|
|
214
|
+
`WHEN NOT EXISTS (SELECT 1 FROM ${ENTITY_STATE_MOVES_TABLE} WHERE entity_type = ${literal(plan.entityType)} AND entity_id = OLD.${plan.idColumn}) BEGIN`,
|
|
215
|
+
` SELECT RAISE(ABORT, 'archive and trash state moves only through ctx.archive, ctx.trash and ctx.restore (#119)');`,
|
|
216
|
+
`END`,
|
|
217
|
+
]),
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Add one module's plans to a scope's registry, refusing a second declaration of a type — the
|
|
222
|
+
* table is one module's, and two answers to "who may trash it" is no answer — and a key the
|
|
223
|
+
* module does not declare. The entity is the module's, so its keys are too: a key nobody
|
|
224
|
+
* declared reaches no role, no grant shape and no `PERMISSIONS.md`, and would make the verb
|
|
225
|
+
* look gated while nobody could ever pass it.
|
|
226
|
+
*/
|
|
227
|
+
export function addStatePlans(byType, moduleId, declarations, declaredKeys) {
|
|
228
|
+
for (const plan of entityStatePlans(moduleId, declarations)) {
|
|
229
|
+
for (const key of [plan.archivePermission, plan.trashPermission]) {
|
|
230
|
+
if (key && !declaredKeys.some((p) => p.key === key)) {
|
|
231
|
+
throw new Error(`entity state: ${moduleId} gates '${plan.entityType}' on '${key}', which it does not declare in \`permissions\``);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const existing = byType.get(plan.entityType);
|
|
235
|
+
if (existing) {
|
|
236
|
+
throw new Error(`entity state: '${plan.entityType}' is declared archivable/trashable by both ` +
|
|
237
|
+
`'${existing.moduleId}' and '${plan.moduleId}' — one entity type, one owner`);
|
|
238
|
+
}
|
|
239
|
+
byType.set(plan.entityType, plan);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** The tables whose rows carry the columns, lowercased as SQLite resolves a name — `guardSpine`'s input. */
|
|
243
|
+
export const statefulTablesOf = (plans) => new Set([...plans.values()].map((p) => p.table.toLowerCase()));
|
|
244
|
+
export const stateColumnsOf = (plan) => ({
|
|
245
|
+
archive: plan.archivePermission !== undefined,
|
|
246
|
+
trash: plan.trashPermission !== undefined,
|
|
247
|
+
});
|
|
248
|
+
/** The views an entity with these columns has — `active` always, then one per column. */
|
|
249
|
+
export function viewsOf(columns) {
|
|
250
|
+
return ['active', ...(columns.archive ? ['archived'] : []), ...(columns.trash ? ['trashed'] : [])];
|
|
251
|
+
}
|
|
252
|
+
export function entityStateWhere(entityType, columns, view, alias) {
|
|
253
|
+
const asked = view ?? 'active';
|
|
254
|
+
if ((asked === 'archived' && !columns?.archive) || (asked === 'trashed' && !columns?.trash)) {
|
|
255
|
+
throw substratError('validation_failed', `'${entityType}' has no ${asked} view — it declares no ${asked === 'archived' ? 'archive' : 'trash'}`, { errors: [{ path: 'view', message: `not declared for '${entityType}'` }] });
|
|
256
|
+
}
|
|
257
|
+
if (!columns)
|
|
258
|
+
return undefined;
|
|
259
|
+
const col = (name) => (alias ? `${alias}.${name}` : name);
|
|
260
|
+
if (asked === 'trashed')
|
|
261
|
+
return `${col(TRASHED_AT_COLUMN)} IS NOT NULL`;
|
|
262
|
+
const terms = [];
|
|
263
|
+
if (columns.archive)
|
|
264
|
+
terms.push(`${col(ARCHIVED_AT_COLUMN)} IS ${asked === 'archived' ? 'NOT ' : ''}NULL`);
|
|
265
|
+
if (columns.trash)
|
|
266
|
+
terms.push(`${col(TRASHED_AT_COLUMN)} IS NULL`);
|
|
267
|
+
return terms.join(' AND ');
|
|
268
|
+
}
|
|
269
|
+
const stateOf = (row) => row.trashed_at !== null ? 'trashed' : row.archived_at !== null ? 'archived' : 'active';
|
|
270
|
+
/** One row's two columns, or `undefined` when the row does not exist. */
|
|
271
|
+
export function readStateRow(sql, plan, entityId) {
|
|
272
|
+
const columns = stateColumnsOf(plan);
|
|
273
|
+
return sql.query(`SELECT ${columns.archive ? ARCHIVED_AT_COLUMN : 'NULL'} AS archived_at, ` +
|
|
274
|
+
`${columns.trash ? TRASHED_AT_COLUMN : 'NULL'} AS trashed_at ` +
|
|
275
|
+
`FROM ${plan.table} WHERE ${plan.idColumn} = ?`, [entityId])[0];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The plan for `entityType`, and the key one of its states is gated on — or `validation_failed`
|
|
279
|
+
* naming what the entity does not declare. Shared by the verbs and the trashed readers, so
|
|
280
|
+
* "declares no trash" is one sentence.
|
|
281
|
+
*/
|
|
282
|
+
export function stateKeyOf(plans, verb, entityType, which) {
|
|
283
|
+
const plan = plans.get(entityType);
|
|
284
|
+
const key = which === 'archive' ? plan?.archivePermission : plan?.trashPermission;
|
|
285
|
+
if (!plan || !key) {
|
|
286
|
+
throw substratError('validation_failed', `${verb}: '${entityType}' declares no ${which}`, {
|
|
287
|
+
errors: [{ path: 'entityType', message: `'${entityType}' declares no ${which}` }],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return { plan, key };
|
|
291
|
+
}
|
|
292
|
+
const COLUMN = { archived_at: ARCHIVED_AT_COLUMN, trashed_at: TRASHED_AT_COLUMN };
|
|
293
|
+
const MOVES = {
|
|
294
|
+
archive: { verb: 'ctx.archive', which: 'archive', from: ['active'], column: 'archived_at', set: true, type: ENTITY_ARCHIVED },
|
|
295
|
+
unarchive: { verb: 'ctx.unarchive', which: 'archive', from: ['archived'], column: 'archived_at', set: false, type: ENTITY_UNARCHIVED },
|
|
296
|
+
// Archived rows may be trashed: "delete this old thing" is the commonest reason to open an
|
|
297
|
+
// archive at all. The archive survives the trip, which is what `restore` relies on.
|
|
298
|
+
trash: { verb: 'ctx.trash', which: 'trash', from: ['active', 'archived'], column: 'trashed_at', set: true, type: ENTITY_TRASHED },
|
|
299
|
+
restore: { verb: 'ctx.restore', which: 'trash', from: ['trashed'], column: 'trashed_at', set: false, type: ENTITY_RESTORED },
|
|
300
|
+
};
|
|
301
|
+
export function createEntityStateVerbs(deps) {
|
|
302
|
+
const move = async (m, entity) => {
|
|
303
|
+
deps.assertWrites(m.verb);
|
|
304
|
+
const { plan, key } = stateKeyOf(deps.plans, m.verb, entity.entityType, m.which);
|
|
305
|
+
// The DECLARED key, on THIS entity, before anything about the row is read: whether it
|
|
306
|
+
// exists is not something a caller without the key gets to learn.
|
|
307
|
+
assertAllowed(await deps.check(key, entity));
|
|
308
|
+
const row = readStateRow(deps.sql, plan, entity.entityId);
|
|
309
|
+
if (!row) {
|
|
310
|
+
throw substratError('not_found', `${m.verb}: ${entity.entityType}:${entity.entityId} does not exist`);
|
|
311
|
+
}
|
|
312
|
+
const from = stateOf(row);
|
|
313
|
+
if (!m.from.includes(from)) {
|
|
314
|
+
throw substratError('conflict', `${m.verb}: ${entity.entityType}:${entity.entityId} is ${from} — ` +
|
|
315
|
+
`only ${m.from.join(' or ')} can be moved this way`, { reason: 'invalid_transition' });
|
|
316
|
+
}
|
|
317
|
+
const at = m.set ? deps.now : null;
|
|
318
|
+
// The authorization the update trigger asks for, held for exactly this one statement.
|
|
319
|
+
const authorize = [entity.entityType, entity.entityId];
|
|
320
|
+
deps.sql.exec(`INSERT OR IGNORE INTO ${ENTITY_STATE_MOVES_TABLE} (entity_type, entity_id) VALUES (?, ?)`, authorize);
|
|
321
|
+
try {
|
|
322
|
+
const { changes } = deps.sql.exec(`UPDATE ${plan.table} SET ${COLUMN[m.column]} = ? WHERE ${plan.idColumn} = ?`, [
|
|
323
|
+
at,
|
|
324
|
+
entity.entityId,
|
|
325
|
+
]);
|
|
326
|
+
// The row exists (read above), so an UPDATE that changed nothing was swallowed — a trigger's
|
|
327
|
+
// RAISE(IGNORE), say. Recording a move that did not happen is the one thing this must not do.
|
|
328
|
+
// `0`, not `!== 1`: a Durable Object's count includes the index rows the move rewrote.
|
|
329
|
+
if (changes === 0) {
|
|
330
|
+
throw substratError('internal', `${m.verb}: the update of ${entity.entityType}:${entity.entityId} changed no row — nothing was recorded`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
deps.sql.exec(`DELETE FROM ${ENTITY_STATE_MOVES_TABLE} WHERE entity_type = ? AND entity_id = ?`, authorize);
|
|
335
|
+
}
|
|
336
|
+
const to = stateOf({ ...row, [m.column]: at });
|
|
337
|
+
const payload = entityStateChangedPayload.parse({ entity, from, to }); // strips extra keys
|
|
338
|
+
deps.emit({ type: m.type, schemaVersion: 1, entity: payload.entity, piiClass: 'none', payload });
|
|
339
|
+
return to;
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
archive: (entity) => move(MOVES.archive, entity),
|
|
343
|
+
unarchive: (entity) => move(MOVES.unarchive, entity),
|
|
344
|
+
trash: (entity) => move(MOVES.trash, entity),
|
|
345
|
+
restore: (entity) => move(MOVES.restore, entity),
|
|
346
|
+
entityState: (entity) => {
|
|
347
|
+
const plan = deps.plans.get(entity.entityType);
|
|
348
|
+
if (!plan) {
|
|
349
|
+
throw substratError('validation_failed', `ctx.entityState: '${entity.entityType}' declares no archive or trash — declare one on the entity`);
|
|
350
|
+
}
|
|
351
|
+
const row = readStateRow(deps.sql, plan, entity.entityId);
|
|
352
|
+
return row ? stateOf(row) : null;
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=entity-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-state.js","sourceRoot":"","sources":["../src/entity-state.ts"],"names":[],"mappings":"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"}
|