@substrat-run/kernel 0.138.0 → 0.139.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/attachment-extractor.d.ts +9 -1
- package/dist/attachment-extractor.d.ts.map +1 -1
- package/dist/attachment-extractor.js +21 -1
- 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/entity-state-reads.d.ts +43 -0
- package/dist/entity-state-reads.d.ts.map +1 -0
- package/dist/entity-state-reads.js +135 -0
- package/dist/entity-state-reads.js.map +1 -0
- package/dist/entity-state.d.ts +177 -0
- package/dist/entity-state.d.ts.map +1 -0
- package/dist/entity-state.js +345 -0
- package/dist/entity-state.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/index.d.ts +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/list-index.d.ts +25 -3
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +70 -26
- package/dist/list-index.js.map +1 -1
- package/dist/membership-executor.d.ts +17 -5
- package/dist/membership-executor.d.ts.map +1 -1
- package/dist/membership-executor.js +26 -11
- package/dist/membership-executor.js.map +1 -1
- 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/permission-eval.d.ts +41 -0
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +53 -0
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-sweep.d.ts +7 -1
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +12 -0
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-host.d.ts +187 -11
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +10 -2
- 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 +13 -1
- package/dist/search-index.d.ts.map +1 -1
- package/dist/search-index.js +9 -16
- package/dist/search-index.js.map +1 -1
- package/dist/spine-guard.d.ts +64 -2
- package/dist/spine-guard.d.ts.map +1 -1
- package/dist/spine-guard.js +345 -10
- package/dist/spine-guard.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/package.json +2 -2
|
@@ -0,0 +1,345 @@
|
|
|
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
|
+
plans.push({
|
|
66
|
+
moduleId,
|
|
67
|
+
entityType: decl.entityType,
|
|
68
|
+
table: assertSqlIdentifier('entity state', 'a table', decl.table, where),
|
|
69
|
+
idColumn: assertSqlIdentifier('entity state', 'an id column', decl.idColumn ?? 'id', where),
|
|
70
|
+
...(decl.archivePermission ? { archivePermission: decl.archivePermission } : {}),
|
|
71
|
+
...(decl.trashPermission ? { trashPermission: decl.trashPermission } : {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return plans;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The migrations that add the columns — one per column, so declaring `trash` on an entity
|
|
78
|
+
* that already declared `archive` adds one column and re-runs nothing.
|
|
79
|
+
*
|
|
80
|
+
* **The version is the declaration**, as for search and list indexes. Unlike theirs, this DDL
|
|
81
|
+
* is not drop-then-create: a column holds the state, and dropping it would un-trash every row.
|
|
82
|
+
* So it runs once per column, ever. Withdrawing a declaration leaves the column where it is,
|
|
83
|
+
* holding what it held; re-declaring it finds the migration already applied.
|
|
84
|
+
*
|
|
85
|
+
* Applied after the module's own migrations (the table must exist) and before its list
|
|
86
|
+
* indexes (their partial `WHERE` names these columns) — `moduleMigrations` writes that order.
|
|
87
|
+
*/
|
|
88
|
+
export function entityStateMigrations(moduleId, declarations) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const plan of entityStatePlans(moduleId, declarations)) {
|
|
91
|
+
if (plan.archivePermission) {
|
|
92
|
+
out.push({
|
|
93
|
+
version: `state/${plan.entityType}:archive`,
|
|
94
|
+
sql: `ALTER TABLE ${plan.table} ADD COLUMN ${ARCHIVED_AT_COLUMN} TEXT;`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (plan.trashPermission) {
|
|
98
|
+
out.push({
|
|
99
|
+
version: `state/${plan.entityType}:trash`,
|
|
100
|
+
sql: `ALTER TABLE ${plan.table} ADD COLUMN ${TRASHED_AT_COLUMN} TEXT;`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// After the columns they name. Versioned by which columns they guard, so declaring a trash
|
|
104
|
+
// on an archivable entity rebuilds them to guard both.
|
|
105
|
+
out.push({ version: `state/${plan.entityType}:guard:${columnNamesOf(plan).join('+')}`, sql: entityStateTriggerDdl(plan) });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
const columnNamesOf = (plan) => [
|
|
110
|
+
...(plan.archivePermission ? ['archive'] : []),
|
|
111
|
+
...(plan.trashPermission ? ['trash'] : []),
|
|
112
|
+
];
|
|
113
|
+
/** The prefix of every trigger this module derives — kernel-owned, so the reserved one. */
|
|
114
|
+
export const ENTITY_STATE_TRIGGER_PREFIX = '_substrat_state_';
|
|
115
|
+
/**
|
|
116
|
+
* The kernel's authorization for ONE move (#119, Codex r2): the row `ctx.archive` & co. write
|
|
117
|
+
* just before their `UPDATE` and delete just after it, in the same transaction. The update
|
|
118
|
+
* trigger below refuses any change to the columns that has no such row — so the only writer the
|
|
119
|
+
* columns admit is the one that can write a `_substrat_*` TABLE, which `ctx.sql` refuses by name
|
|
120
|
+
* (a far simpler reading than finding an assignment target in an expression).
|
|
121
|
+
*
|
|
122
|
+
* Never holds a row between operations: written and removed inside one move, and a move that
|
|
123
|
+
* throws rolls back with its operation. Shared by both adapters' `KERNEL_DDL`, like the other
|
|
124
|
+
* kernel-owned spine tables, so the two cannot part company.
|
|
125
|
+
*/
|
|
126
|
+
export const ENTITY_STATE_MOVES_TABLE = '_substrat_state_moves';
|
|
127
|
+
export const ENTITY_STATE_MOVES_DDL = `
|
|
128
|
+
CREATE TABLE IF NOT EXISTS _substrat_state_moves (
|
|
129
|
+
entity_type TEXT NOT NULL,
|
|
130
|
+
entity_id TEXT NOT NULL,
|
|
131
|
+
PRIMARY KEY (entity_type, entity_id)
|
|
132
|
+
);
|
|
133
|
+
`;
|
|
134
|
+
/** A string literal for SQL text: the entity type is a declaration, still quoted rather than trusted. */
|
|
135
|
+
const literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
136
|
+
/**
|
|
137
|
+
* The invariant below the column guard, as two triggers on the entity's table.
|
|
138
|
+
*
|
|
139
|
+
* `ctx.sql` refuses the writes that would set the columns (`assertNoReservedColumnWrite`), but
|
|
140
|
+
* that is a reading of SQL text, and a reading can miss a form — it did, twice. These hold
|
|
141
|
+
* whatever the text says:
|
|
142
|
+
*
|
|
143
|
+
* - **born** — `BEFORE INSERT`: a row is never inserted archived or trashed. Every row enters
|
|
144
|
+
* active.
|
|
145
|
+
* - **moved** — `BEFORE UPDATE OF` the columns: a change to either aborts unless the kernel's
|
|
146
|
+
* authorization row for this entity exists (`ENTITY_STATE_MOVES_TABLE`), which only a move
|
|
147
|
+
* writes. An `UPDATE` that does not name the columns does not fire it.
|
|
148
|
+
*
|
|
149
|
+
* Drop-then-create, like the search triggers, and re-run after a dump load: a load drops the
|
|
150
|
+
* table and with it the triggers, then inserts the rows first — a restored binned row is
|
|
151
|
+
* legitimately born trashed, so the triggers are put back only after them.
|
|
152
|
+
*/
|
|
153
|
+
export function entityStateTriggerDdl(plan) {
|
|
154
|
+
const born = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_born`;
|
|
155
|
+
const moved = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_moved`;
|
|
156
|
+
const columns = [
|
|
157
|
+
...(plan.archivePermission ? [ARCHIVED_AT_COLUMN] : []),
|
|
158
|
+
...(plan.trashPermission ? [TRASHED_AT_COLUMN] : []),
|
|
159
|
+
];
|
|
160
|
+
return [
|
|
161
|
+
`DROP TRIGGER IF EXISTS ${born};`,
|
|
162
|
+
`CREATE TRIGGER ${born} BEFORE INSERT ON ${plan.table} WHEN ${columns.map((c) => `NEW.${c} IS NOT NULL`).join(' OR ')} BEGIN`,
|
|
163
|
+
` SELECT RAISE(ABORT, 'a row is never inserted archived or trashed - ctx.archive and ctx.trash move it (#119)');`,
|
|
164
|
+
`END;`,
|
|
165
|
+
`DROP TRIGGER IF EXISTS ${moved};`,
|
|
166
|
+
`CREATE TRIGGER ${moved} BEFORE UPDATE OF ${columns.join(', ')} ON ${plan.table}`,
|
|
167
|
+
`WHEN NOT EXISTS (SELECT 1 FROM ${ENTITY_STATE_MOVES_TABLE} WHERE entity_type = ${literal(plan.entityType)} AND entity_id = OLD.${plan.idColumn}) BEGIN`,
|
|
168
|
+
` SELECT RAISE(ABORT, 'archive and trash state moves only through ctx.archive, ctx.trash and ctx.restore (#119)');`,
|
|
169
|
+
`END;`,
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Add one module's plans to a scope's registry, refusing a second declaration of a type — the
|
|
174
|
+
* table is one module's, and two answers to "who may trash it" is no answer — and a key the
|
|
175
|
+
* module does not declare. The entity is the module's, so its keys are too: a key nobody
|
|
176
|
+
* declared reaches no role, no grant shape and no `PERMISSIONS.md`, and would make the verb
|
|
177
|
+
* look gated while nobody could ever pass it.
|
|
178
|
+
*/
|
|
179
|
+
export function addStatePlans(byType, moduleId, declarations, declaredKeys) {
|
|
180
|
+
for (const plan of entityStatePlans(moduleId, declarations)) {
|
|
181
|
+
for (const key of [plan.archivePermission, plan.trashPermission]) {
|
|
182
|
+
if (key && !declaredKeys.some((p) => p.key === key)) {
|
|
183
|
+
throw new Error(`entity state: ${moduleId} gates '${plan.entityType}' on '${key}', which it does not declare in \`permissions\``);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const existing = byType.get(plan.entityType);
|
|
187
|
+
if (existing) {
|
|
188
|
+
throw new Error(`entity state: '${plan.entityType}' is declared archivable/trashable by both ` +
|
|
189
|
+
`'${existing.moduleId}' and '${plan.moduleId}' — one entity type, one owner`);
|
|
190
|
+
}
|
|
191
|
+
byType.set(plan.entityType, plan);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** The tables whose rows carry the columns, lowercased as SQLite resolves a name — `guardSpine`'s input. */
|
|
195
|
+
export const statefulTablesOf = (plans) => new Set([...plans.values()].map((p) => p.table.toLowerCase()));
|
|
196
|
+
export const stateColumnsOf = (plan) => ({
|
|
197
|
+
archive: plan.archivePermission !== undefined,
|
|
198
|
+
trash: plan.trashPermission !== undefined,
|
|
199
|
+
});
|
|
200
|
+
/** The views an entity with these columns has — `active` always, then one per column. */
|
|
201
|
+
export function viewsOf(columns) {
|
|
202
|
+
return ['active', ...(columns.archive ? ['archived'] : []), ...(columns.trash ? ['trashed'] : [])];
|
|
203
|
+
}
|
|
204
|
+
export function entityStateWhere(entityType, columns, view, alias) {
|
|
205
|
+
const asked = view ?? 'active';
|
|
206
|
+
if ((asked === 'archived' && !columns?.archive) || (asked === 'trashed' && !columns?.trash)) {
|
|
207
|
+
throw substratError('validation_failed', `'${entityType}' has no ${asked} view — it declares no ${asked === 'archived' ? 'archive' : 'trash'}`, { errors: [{ path: 'view', message: `not declared for '${entityType}'` }] });
|
|
208
|
+
}
|
|
209
|
+
if (!columns)
|
|
210
|
+
return undefined;
|
|
211
|
+
const col = (name) => (alias ? `${alias}.${name}` : name);
|
|
212
|
+
if (asked === 'trashed')
|
|
213
|
+
return `${col(TRASHED_AT_COLUMN)} IS NOT NULL`;
|
|
214
|
+
const terms = [];
|
|
215
|
+
if (columns.archive)
|
|
216
|
+
terms.push(`${col(ARCHIVED_AT_COLUMN)} IS ${asked === 'archived' ? 'NOT ' : ''}NULL`);
|
|
217
|
+
if (columns.trash)
|
|
218
|
+
terms.push(`${col(TRASHED_AT_COLUMN)} IS NULL`);
|
|
219
|
+
return terms.join(' AND ');
|
|
220
|
+
}
|
|
221
|
+
const stateOf = (row) => row.trashed_at !== null ? 'trashed' : row.archived_at !== null ? 'archived' : 'active';
|
|
222
|
+
/** One row's two columns, or `undefined` when the row does not exist. */
|
|
223
|
+
function readStateRow(sql, plan, entityId) {
|
|
224
|
+
const columns = stateColumnsOf(plan);
|
|
225
|
+
return sql.query(`SELECT ${columns.archive ? ARCHIVED_AT_COLUMN : 'NULL'} AS archived_at, ` +
|
|
226
|
+
`${columns.trash ? TRASHED_AT_COLUMN : 'NULL'} AS trashed_at ` +
|
|
227
|
+
`FROM ${plan.table} WHERE ${plan.idColumn} = ?`, [entityId])[0];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The plan for `entityType`, and the key one of its states is gated on — or `validation_failed`
|
|
231
|
+
* naming what the entity does not declare. Shared by the verbs and the trashed readers, so
|
|
232
|
+
* "declares no trash" is one sentence.
|
|
233
|
+
*/
|
|
234
|
+
export function stateKeyOf(plans, verb, entityType, which) {
|
|
235
|
+
const plan = plans.get(entityType);
|
|
236
|
+
const key = which === 'archive' ? plan?.archivePermission : plan?.trashPermission;
|
|
237
|
+
if (!plan || !key) {
|
|
238
|
+
throw substratError('validation_failed', `${verb}: '${entityType}' declares no ${which}`, {
|
|
239
|
+
errors: [{ path: 'entityType', message: `'${entityType}' declares no ${which}` }],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return { plan, key };
|
|
243
|
+
}
|
|
244
|
+
const COLUMN = { archived_at: ARCHIVED_AT_COLUMN, trashed_at: TRASHED_AT_COLUMN };
|
|
245
|
+
const MOVES = {
|
|
246
|
+
archive: { verb: 'ctx.archive', which: 'archive', from: ['active'], column: 'archived_at', set: true, type: ENTITY_ARCHIVED },
|
|
247
|
+
unarchive: { verb: 'ctx.unarchive', which: 'archive', from: ['archived'], column: 'archived_at', set: false, type: ENTITY_UNARCHIVED },
|
|
248
|
+
// Archived rows may be trashed: "delete this old thing" is the commonest reason to open an
|
|
249
|
+
// archive at all. The archive survives the trip, which is what `restore` relies on.
|
|
250
|
+
trash: { verb: 'ctx.trash', which: 'trash', from: ['active', 'archived'], column: 'trashed_at', set: true, type: ENTITY_TRASHED },
|
|
251
|
+
restore: { verb: 'ctx.restore', which: 'trash', from: ['trashed'], column: 'trashed_at', set: false, type: ENTITY_RESTORED },
|
|
252
|
+
};
|
|
253
|
+
export function createEntityStateVerbs(deps) {
|
|
254
|
+
const move = async (m, entity) => {
|
|
255
|
+
deps.assertWrites(m.verb);
|
|
256
|
+
const { plan, key } = stateKeyOf(deps.plans, m.verb, entity.entityType, m.which);
|
|
257
|
+
// The DECLARED key, on THIS entity, before anything about the row is read: whether it
|
|
258
|
+
// exists is not something a caller without the key gets to learn.
|
|
259
|
+
assertAllowed(await deps.check(key, entity));
|
|
260
|
+
const row = readStateRow(deps.sql, plan, entity.entityId);
|
|
261
|
+
if (!row) {
|
|
262
|
+
throw substratError('not_found', `${m.verb}: ${entity.entityType}:${entity.entityId} does not exist`);
|
|
263
|
+
}
|
|
264
|
+
const from = stateOf(row);
|
|
265
|
+
if (!m.from.includes(from)) {
|
|
266
|
+
throw substratError('conflict', `${m.verb}: ${entity.entityType}:${entity.entityId} is ${from} — ` +
|
|
267
|
+
`only ${m.from.join(' or ')} can be moved this way`, { reason: 'invalid_transition' });
|
|
268
|
+
}
|
|
269
|
+
const at = m.set ? deps.now : null;
|
|
270
|
+
// The authorization the update trigger asks for, held for exactly this one statement.
|
|
271
|
+
const authorize = [entity.entityType, entity.entityId];
|
|
272
|
+
deps.sql.exec(`INSERT OR IGNORE INTO ${ENTITY_STATE_MOVES_TABLE} (entity_type, entity_id) VALUES (?, ?)`, authorize);
|
|
273
|
+
try {
|
|
274
|
+
const { changes } = deps.sql.exec(`UPDATE ${plan.table} SET ${COLUMN[m.column]} = ? WHERE ${plan.idColumn} = ?`, [
|
|
275
|
+
at,
|
|
276
|
+
entity.entityId,
|
|
277
|
+
]);
|
|
278
|
+
// The row exists (read above), so an UPDATE that changed nothing was swallowed — a trigger's
|
|
279
|
+
// RAISE(IGNORE), say. Recording a move that did not happen is the one thing this must not do.
|
|
280
|
+
// `0`, not `!== 1`: a Durable Object's count includes the index rows the move rewrote.
|
|
281
|
+
if (changes === 0) {
|
|
282
|
+
throw substratError('internal', `${m.verb}: the update of ${entity.entityType}:${entity.entityId} changed no row — nothing was recorded`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
deps.sql.exec(`DELETE FROM ${ENTITY_STATE_MOVES_TABLE} WHERE entity_type = ? AND entity_id = ?`, authorize);
|
|
287
|
+
}
|
|
288
|
+
const to = stateOf({ ...row, [m.column]: at });
|
|
289
|
+
const payload = entityStateChangedPayload.parse({ entity, from, to }); // strips extra keys
|
|
290
|
+
deps.emit({ type: m.type, schemaVersion: 1, entity: payload.entity, piiClass: 'none', payload });
|
|
291
|
+
return to;
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
archive: (entity) => move(MOVES.archive, entity),
|
|
295
|
+
unarchive: (entity) => move(MOVES.unarchive, entity),
|
|
296
|
+
trash: (entity) => move(MOVES.trash, entity),
|
|
297
|
+
restore: (entity) => move(MOVES.restore, entity),
|
|
298
|
+
entityState: (entity) => {
|
|
299
|
+
const plan = deps.plans.get(entity.entityType);
|
|
300
|
+
if (!plan) {
|
|
301
|
+
throw substratError('validation_failed', `ctx.entityState: '${entity.entityType}' declares no archive or trash — declare one on the entity`);
|
|
302
|
+
}
|
|
303
|
+
const row = readStateRow(deps.sql, plan, entity.entityId);
|
|
304
|
+
return row ? stateOf(row) : null;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* After runtime DDL (#119, Codex r3): does every stateful table still carry what the kernel
|
|
310
|
+
* derived for it? The guard refuses the DDL known to take it away; this is the check that does not
|
|
311
|
+
* depend on having known. Read from `main`'s own catalogue, so a same-named temp object cannot
|
|
312
|
+
* stand in for the real table. Throws — the operation and its DDL roll back — rather than
|
|
313
|
+
* re-deriving: a schema the kernel did not expect is not one to repair silently.
|
|
314
|
+
*
|
|
315
|
+
* `indexes` names the derived list indexes each stateful table must keep, by table.
|
|
316
|
+
*/
|
|
317
|
+
export function assertEntityStateIntact(sql, plans, indexes = new Map()) {
|
|
318
|
+
for (const plan of plans.values()) {
|
|
319
|
+
const fail = (what) => {
|
|
320
|
+
throw substratError('internal', `runtime DDL left '${plan.table}' without ${what} — its archive/trash guarantees depend on it; nothing was changed`);
|
|
321
|
+
};
|
|
322
|
+
const objects = new Map(sql
|
|
323
|
+
.query(`SELECT type, name FROM main.sqlite_master WHERE tbl_name = ? COLLATE NOCASE`, [plan.table])
|
|
324
|
+
.map((r) => [r.name.toLowerCase(), r.type]));
|
|
325
|
+
if (objects.get(plan.table.toLowerCase()) !== 'table')
|
|
326
|
+
fail('its table');
|
|
327
|
+
const columns = new Set(sql
|
|
328
|
+
.query(`SELECT name FROM pragma_table_info(?, 'main')`, [plan.table])
|
|
329
|
+
.map((r) => r.name.toLowerCase()));
|
|
330
|
+
if (plan.archivePermission && !columns.has(ARCHIVED_AT_COLUMN))
|
|
331
|
+
fail(ARCHIVED_AT_COLUMN);
|
|
332
|
+
if (plan.trashPermission && !columns.has(TRASHED_AT_COLUMN))
|
|
333
|
+
fail(TRASHED_AT_COLUMN);
|
|
334
|
+
for (const suffix of ['born', 'moved']) {
|
|
335
|
+
const trigger = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_${suffix}`.toLowerCase();
|
|
336
|
+
if (objects.get(trigger) !== 'trigger')
|
|
337
|
+
fail(`its ${suffix} trigger`);
|
|
338
|
+
}
|
|
339
|
+
for (const index of indexes.get(plan.table) ?? []) {
|
|
340
|
+
if (objects.get(index.toLowerCase()) !== 'index')
|
|
341
|
+
fail(`its list index ${index}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
//# 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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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type ErrorCode, type FindingEntry, type FindingEvidence, type FindingFilter, type FindingKind, type FindingRuleEntry, type FindingRuleInput, type FindingSeverity, type FindingStatusInput, type TenantId } from '@substrat-run/contracts';
|
|
2
|
+
import type { RedactionSql } from './subject-redaction.js';
|
|
3
|
+
/**
|
|
4
|
+
* How long a finding outlives its last occurrence — the issue store's window, since a Recurring
|
|
5
|
+
* finding is the tenant's projection of an issue and should be recognisable for as long.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FINDING_RETENTION_DAYS = 180;
|
|
8
|
+
/**
|
|
9
|
+
* The findings store (#1748), as statements both adapters run against their directory — the
|
|
10
|
+
* SQLite host over better-sqlite3, the hosted one inside ControlPlaneDO — so the lifecycle is
|
|
11
|
+
* written once. `sql` is the row-returning executor subject erasure already uses.
|
|
12
|
+
*
|
|
13
|
+
* Detection is ON WRITE: the call that records an ops failure or a sweep run also observes the
|
|
14
|
+
* finding it implies, so there is no cron and no scan. Per occurrence that is one indexed rule
|
|
15
|
+
* lookup and one primary-key upsert, over evidence that is already bounded upstream.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FINDINGS_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_findings (\n id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n subject TEXT NOT NULL,\n status TEXT NOT NULL,\n regressed INTEGER NOT NULL DEFAULT 0,\n severity TEXT NOT NULL,\n title TEXT NOT NULL,\n operation TEXT,\n codes TEXT NOT NULL DEFAULT '[]',\n vertical TEXT,\n scope_id TEXT,\n seen_count INTEGER NOT NULL,\n first_seen TEXT NOT NULL,\n last_seen TEXT NOT NULL,\n last_version TEXT,\n resolved_version TEXT,\n resolved_at TEXT,\n resolution TEXT,\n acknowledged_at TEXT,\n rule_id TEXT,\n evidence TEXT NOT NULL,\n likely_cause TEXT,\n UNIQUE (tenant_id, kind, subject)\n );\n CREATE INDEX IF NOT EXISTS _substrat_findings_tenant_seen ON _substrat_findings (tenant_id, last_seen);\n CREATE INDEX IF NOT EXISTS _substrat_findings_seen ON _substrat_findings (last_seen);\n CREATE INDEX IF NOT EXISTS _substrat_findings_quiet ON _substrat_findings (last_seen) WHERE status IN ('open', 'acked');\n CREATE TABLE IF NOT EXISTS _substrat_finding_rules (\n id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n kind TEXT,\n operation TEXT,\n code TEXT,\n subject TEXT,\n expires_at TEXT NOT NULL,\n reason TEXT NOT NULL,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _substrat_finding_rules_tenant ON _substrat_finding_rules (tenant_id, expires_at);\n CREATE INDEX IF NOT EXISTS _substrat_finding_rules_expiry ON _substrat_finding_rules (expires_at);\n";
|
|
18
|
+
/** One occurrence, as a source hands it to `observeFinding`. */
|
|
19
|
+
export interface FindingObservation {
|
|
20
|
+
tenantId: string;
|
|
21
|
+
kind: FindingKind;
|
|
22
|
+
subject: string;
|
|
23
|
+
severity: FindingSeverity;
|
|
24
|
+
title: string;
|
|
25
|
+
operation: string | null;
|
|
26
|
+
code: ErrorCode | null;
|
|
27
|
+
vertical: string | null;
|
|
28
|
+
scopeId: string | null;
|
|
29
|
+
version: string | null;
|
|
30
|
+
evidence: FindingEvidence;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The Recurring source: a tenant's own ops failure, keyed on the same fingerprint as the fleet
|
|
34
|
+
* issue. A failure with no tenant is the platform's own and stays in the staff issue store.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findingOfOpsFailure(f: {
|
|
37
|
+
tenantId?: string | null;
|
|
38
|
+
scopeId?: string | null;
|
|
39
|
+
operation: string;
|
|
40
|
+
stage?: string | null;
|
|
41
|
+
code?: ErrorCode | null;
|
|
42
|
+
vertical?: string | null;
|
|
43
|
+
version?: string | null;
|
|
44
|
+
}): FindingObservation | null;
|
|
45
|
+
/**
|
|
46
|
+
* The Invariant and Drift sources: a scheduled run that failed (expected 0 failed runs,
|
|
47
|
+
* observed N) and a freshness expectation judged stale. Every other sweep row implies nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findingOfSweepRun(r: {
|
|
50
|
+
kind: string;
|
|
51
|
+
unit: string;
|
|
52
|
+
outcome: string;
|
|
53
|
+
tenantId?: string | null;
|
|
54
|
+
scopeId?: string | null;
|
|
55
|
+
vertical?: string | null;
|
|
56
|
+
version?: string | null;
|
|
57
|
+
operation?: string | null;
|
|
58
|
+
eventType?: string | null;
|
|
59
|
+
}): FindingObservation | null;
|
|
60
|
+
/**
|
|
61
|
+
* Record one occurrence on its finding, opening it if new. The transitions, in order:
|
|
62
|
+
* - an unexpired rule covers it → `suppressed` (counted all the same: suppressing hides the
|
|
63
|
+
* finding, never the evidence);
|
|
64
|
+
* - it was `resolved` → `open`, `regressed`, and its likely cause is worked out afresh from
|
|
65
|
+
* THIS regression: the version it came back under, when that is not the one it was resolved
|
|
66
|
+
* under, and none otherwise — never a cause left over from an earlier cycle;
|
|
67
|
+
* - it was `suppressed` and no rule covers it any longer → `open`;
|
|
68
|
+
* - otherwise the status stands (`acked` stays acked: somebody is already on it).
|
|
69
|
+
*/
|
|
70
|
+
export declare function observeFinding(sql: RedactionSql, o: FindingObservation, at: string): void;
|
|
71
|
+
/**
|
|
72
|
+
* Findings, most recently seen first. No cursor, like the issue read it projects: the
|
|
73
|
+
* cardinality is the number of distinct shapes a tenant has, and `limit` bounds it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function listFindings(sql: RedactionSql, filter?: FindingFilter): FindingEntry[];
|
|
76
|
+
export interface FindingChange {
|
|
77
|
+
before: FindingEntry;
|
|
78
|
+
after: FindingEntry;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The audit row a findings mutation writes — `recordAdmin`'s action, target, before and after.
|
|
82
|
+
* Every mutation below hands it to `audit` INSIDE its own statements, so the adapter that runs
|
|
83
|
+
* them in one unit commits the change and its row together, or neither (#1748).
|
|
84
|
+
*/
|
|
85
|
+
export interface FindingAudit {
|
|
86
|
+
action: 'setFindingStatus' | 'createFindingRule' | 'revokeFindingRule' | 'resolveStaleFinding';
|
|
87
|
+
target: {
|
|
88
|
+
tenantId: TenantId;
|
|
89
|
+
vertical: string | null;
|
|
90
|
+
};
|
|
91
|
+
before: unknown;
|
|
92
|
+
after: unknown;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A person's verdict: acknowledge, resolve, or reopen. Keyed on (tenant, id), so a tenant can
|
|
96
|
+
* never move another tenant's finding by guessing its id. Reopening a suppressed finding lifts
|
|
97
|
+
* it out of its rule for now; the next occurrence the rule still covers suppresses it again —
|
|
98
|
+
* revoking the rule is what ends a suppression.
|
|
99
|
+
*/
|
|
100
|
+
export declare function setFindingStatus(sql: RedactionSql, tenantId: TenantId, id: string, status: FindingStatusInput, at: string, audit: (row: FindingAudit) => void): FindingChange | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Create a suppress rule and apply it to the findings it covers now: each one not already
|
|
103
|
+
* resolved becomes `suppressed` under it. Returns the rule and the ids it suppressed, for the
|
|
104
|
+
* caller's audit row. The input is parsed by the caller (`findingRuleInput`); the expiry is
|
|
105
|
+
* held here (`assertFindingRuleExpiry`), against the same `at` every other write in the call uses.
|
|
106
|
+
*/
|
|
107
|
+
export declare function createFindingRule(sql: RedactionSql, tenantId: TenantId, input: FindingRuleInput, createdBy: string, at: string, audit: (row: FindingAudit) => void): {
|
|
108
|
+
rule: FindingRuleEntry;
|
|
109
|
+
suppressed: string[];
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* End a rule now. Its findings stay suppressed until their next occurrence, which then opens
|
|
113
|
+
* them — the same thing an expiry does, brought forward.
|
|
114
|
+
*/
|
|
115
|
+
export declare function revokeFindingRule(sql: RedactionSql, tenantId: TenantId, ruleId: string, at: string, audit: (row: FindingAudit) => void): {
|
|
116
|
+
before: FindingRuleEntry;
|
|
117
|
+
after: FindingRuleEntry;
|
|
118
|
+
} | undefined;
|
|
119
|
+
/** A tenant's rules, newest first; `activeAt` keeps only those unexpired at that instant. */
|
|
120
|
+
export declare function listFindingRules(sql: RedactionSql, tenantId: TenantId | undefined, activeAt?: string, limit?: number): FindingRuleEntry[];
|
|
121
|
+
/** What one `pruneFindings` pass did. */
|
|
122
|
+
export interface FindingPruneReport {
|
|
123
|
+
/** Resolved and suppressed findings deleted. */
|
|
124
|
+
deleted: number;
|
|
125
|
+
/** Open or acked findings resolved as `stale`, each audited. */
|
|
126
|
+
staled: number;
|
|
127
|
+
/** Expired suppress rules deleted. */
|
|
128
|
+
rulesDeleted: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The findings retention pass, bounded by `limit` per step, oldest first. Run in ONE unit with
|
|
132
|
+
* `audit`, which the adapter points at its own admin-log write, so a stale resolution never
|
|
133
|
+
* lands without its row.
|
|
134
|
+
*
|
|
135
|
+
* An `open` or `acked` finding is never deleted for going quiet: deleting it would take it out
|
|
136
|
+
* of the inbox of whoever was watching it with nothing said. One quiet for the whole
|
|
137
|
+
* `FINDING_RETENTION_DAYS` is RESOLVED as `stale` instead, and audited. A `resolved` or
|
|
138
|
+
* `suppressed` finding is deleted once both its last occurrence and its last resolve are past
|
|
139
|
+
* the horizon — so a stale-resolved finding stays readable for one more window. An expired rule
|
|
140
|
+
* is deleted once its expiry is past the horizon; the admin log keeps its story.
|
|
141
|
+
*/
|
|
142
|
+
export declare function pruneFindings(sql: RedactionSql, nowMs: number, limit: number, audit: (row: FindingAudit) => void): FindingPruneReport;
|
|
143
|
+
//# sourceMappingURL=findings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"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"}
|