@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,189 @@
|
|
|
1
|
+
import { connectLink, } from '@substrat-run/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* The platform-held connect link (connections.md §3.5.4), as statements both adapters run
|
|
4
|
+
* against their directory — the SQLite host over better-sqlite3, the hosted one inside
|
|
5
|
+
* ControlPlaneDO — so the lifecycle is written once, the way the findings store is.
|
|
6
|
+
*
|
|
7
|
+
* A vertical mints one to be MAILED: a bookkeeping bureau's staff send it to a client
|
|
8
|
+
* company's Fortnox administrator, who opens it days later. A signature alone cannot be
|
|
9
|
+
* withdrawn and cannot be spent, which is what §3.5.3's in-session URL gets away with by
|
|
10
|
+
* living minutes; a link living a week cannot. So the signed state names this row, and the
|
|
11
|
+
* row decides: outstanding and unexpired opens, anything else refuses.
|
|
12
|
+
*
|
|
13
|
+
* Every read and write is keyed by (tenant, scope, id). A link of another scope or tenant
|
|
14
|
+
* is not a forbidden row — it is an absent one, and answers exactly as `unknown` does.
|
|
15
|
+
*
|
|
16
|
+
* Expiry is judged against the `now` the caller passes: the pure host's clock, or the
|
|
17
|
+
* coordinator's wall clock on the hosted one. ISO 8601 UTC text on both sides, so a string
|
|
18
|
+
* comparison is a chronological one.
|
|
19
|
+
*/
|
|
20
|
+
export const CONNECT_LINKS_DDL = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS _substrat_connect_links (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
tenant_id TEXT NOT NULL,
|
|
24
|
+
scope_id TEXT NOT NULL,
|
|
25
|
+
vertical TEXT NOT NULL,
|
|
26
|
+
provider TEXT NOT NULL,
|
|
27
|
+
status TEXT NOT NULL,
|
|
28
|
+
created_by TEXT NOT NULL,
|
|
29
|
+
subject_ref TEXT,
|
|
30
|
+
return_url TEXT,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
expires_at TEXT NOT NULL,
|
|
33
|
+
used_at TEXT,
|
|
34
|
+
account_ref TEXT,
|
|
35
|
+
account_label TEXT
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS _substrat_connect_links_scope
|
|
38
|
+
ON _substrat_connect_links (tenant_id, scope_id, created_at);
|
|
39
|
+
`;
|
|
40
|
+
/** Named, never `*`: the row a read returns is the shape below, whatever the table grows. */
|
|
41
|
+
const COLUMNS = 'id, tenant_id, scope_id, vertical, provider, status, created_by, subject_ref, return_url, ' +
|
|
42
|
+
'created_at, expires_at, used_at, account_ref, account_label';
|
|
43
|
+
const toConnectLink = (r) => connectLink.parse({
|
|
44
|
+
id: r.id,
|
|
45
|
+
tenantId: r.tenant_id,
|
|
46
|
+
scopeId: r.scope_id,
|
|
47
|
+
vertical: r.vertical,
|
|
48
|
+
provider: r.provider,
|
|
49
|
+
status: r.status,
|
|
50
|
+
createdBy: r.created_by,
|
|
51
|
+
subjectRef: r.subject_ref,
|
|
52
|
+
returnUrl: r.return_url,
|
|
53
|
+
createdAt: r.created_at,
|
|
54
|
+
expiresAt: r.expires_at,
|
|
55
|
+
usedAt: r.used_at,
|
|
56
|
+
accountRef: r.account_ref,
|
|
57
|
+
accountLabel: r.account_label,
|
|
58
|
+
});
|
|
59
|
+
const targetOf = (link) => ({ tenantId: link.tenantId, scopeId: link.scopeId, vertical: link.vertical });
|
|
60
|
+
const readRow = (sql, key) => sql(`SELECT ${COLUMNS} FROM _substrat_connect_links WHERE id = ? AND tenant_id = ? AND scope_id = ?`, [
|
|
61
|
+
key.id,
|
|
62
|
+
key.tenantId,
|
|
63
|
+
key.scopeId,
|
|
64
|
+
])[0];
|
|
65
|
+
export function insertConnectLink(sql, row, audit) {
|
|
66
|
+
const inserted = sql(`INSERT INTO _substrat_connect_links
|
|
67
|
+
(id, tenant_id, scope_id, vertical, provider, status, created_by, subject_ref, return_url,
|
|
68
|
+
created_at, expires_at, used_at, account_ref, account_label)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, 'outstanding', ?, ?, ?, ?, ?, NULL, NULL, NULL)
|
|
70
|
+
RETURNING ${COLUMNS}`, [
|
|
71
|
+
row.id,
|
|
72
|
+
row.tenantId,
|
|
73
|
+
row.scopeId,
|
|
74
|
+
row.vertical,
|
|
75
|
+
row.provider,
|
|
76
|
+
row.createdBy,
|
|
77
|
+
row.subjectRef,
|
|
78
|
+
row.returnUrl,
|
|
79
|
+
row.createdAt,
|
|
80
|
+
row.expiresAt,
|
|
81
|
+
])[0];
|
|
82
|
+
const link = toConnectLink(inserted);
|
|
83
|
+
audit({
|
|
84
|
+
action: 'mintConnectLink',
|
|
85
|
+
target: targetOf(link),
|
|
86
|
+
before: null,
|
|
87
|
+
after: { id: link.id, provider: link.provider, createdBy: link.createdBy, expiresAt: link.expiresAt },
|
|
88
|
+
});
|
|
89
|
+
return link;
|
|
90
|
+
}
|
|
91
|
+
export function readConnectLink(sql, key) {
|
|
92
|
+
const row = readRow(sql, key);
|
|
93
|
+
return row ? toConnectLink(row) : undefined;
|
|
94
|
+
}
|
|
95
|
+
export function listConnectLinks(sql, filter, now) {
|
|
96
|
+
if (filter.ids?.length === 0)
|
|
97
|
+
return []; // no id named — match nothing, never everything
|
|
98
|
+
const where = ['tenant_id = ?'];
|
|
99
|
+
const params = [filter.tenantId];
|
|
100
|
+
if (filter.scopeId)
|
|
101
|
+
(where.push('scope_id = ?'), params.push(filter.scopeId));
|
|
102
|
+
// ONE bound JSON array, not a `?` per id: a DO binds at most 100 parameters (#1776).
|
|
103
|
+
if (filter.ids)
|
|
104
|
+
(where.push('id IN (SELECT value FROM json_each(?))'), params.push(JSON.stringify(filter.ids)));
|
|
105
|
+
if (filter.provider)
|
|
106
|
+
(where.push('provider = ?'), params.push(filter.provider));
|
|
107
|
+
if (filter.outstandingOnly)
|
|
108
|
+
(where.push(`status = 'outstanding' AND expires_at > ?`), params.push(now));
|
|
109
|
+
return sql(`SELECT ${COLUMNS} FROM _substrat_connect_links WHERE ${where.join(' AND ')}
|
|
110
|
+
ORDER BY created_at DESC, id DESC`, params).map(toConnectLink);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Withdraw an outstanding link. Idempotent, and only an outstanding row moves: a used link
|
|
114
|
+
* has already been spent (its connection is what to disconnect), and a revoked one stays
|
|
115
|
+
* revoked. `undefined` when no such link exists in this scope; `changed` says whether this
|
|
116
|
+
* call is the one that revoked it, so a no-op is not audited.
|
|
117
|
+
*/
|
|
118
|
+
export function revokeConnectLinkRow(sql, key, audit) {
|
|
119
|
+
const moved = sql(`UPDATE _substrat_connect_links SET status = 'revoked'
|
|
120
|
+
WHERE id = ? AND tenant_id = ? AND scope_id = ? AND status = 'outstanding'
|
|
121
|
+
RETURNING ${COLUMNS}`, [key.id, key.tenantId, key.scopeId])[0];
|
|
122
|
+
if (moved) {
|
|
123
|
+
const link = toConnectLink(moved);
|
|
124
|
+
audit({
|
|
125
|
+
action: 'revokeConnectLink',
|
|
126
|
+
target: targetOf(link),
|
|
127
|
+
before: { status: 'outstanding' },
|
|
128
|
+
after: { id: link.id, provider: link.provider, status: 'revoked' },
|
|
129
|
+
});
|
|
130
|
+
return { link, changed: true };
|
|
131
|
+
}
|
|
132
|
+
const row = readRow(sql, key);
|
|
133
|
+
return row ? { link: toConnectLink(row), changed: false } : undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Spend a link — the callback's act, BEFORE it stores the credential, so of two racing
|
|
137
|
+
* callbacks exactly one gets past it. Single-use is decided by the UPDATE's WHERE, not by
|
|
138
|
+
* the read in front of it: the read only picks which refusal to answer with.
|
|
139
|
+
*/
|
|
140
|
+
export function consumeConnectLinkRow(sql, input, now, audit) {
|
|
141
|
+
const row = readRow(sql, input);
|
|
142
|
+
const refuse = (reason) => ({ ok: false, reason });
|
|
143
|
+
// A link minted for another provider is not this round's link at all.
|
|
144
|
+
if (!row || row.provider !== input.provider)
|
|
145
|
+
return refuse('unknown');
|
|
146
|
+
if (row.status === 'used')
|
|
147
|
+
return refuse('used');
|
|
148
|
+
if (row.status === 'revoked')
|
|
149
|
+
return refuse('revoked');
|
|
150
|
+
if (row.expires_at <= now)
|
|
151
|
+
return refuse('expired');
|
|
152
|
+
const spent = sql(`UPDATE _substrat_connect_links SET status = 'used', used_at = ?, account_ref = ?, account_label = ?
|
|
153
|
+
WHERE id = ? AND tenant_id = ? AND scope_id = ? AND status = 'outstanding' AND expires_at > ?
|
|
154
|
+
RETURNING ${COLUMNS}`, [now, input.accountRef ?? null, input.accountLabel ?? null, input.id, input.tenantId, input.scopeId, now])[0];
|
|
155
|
+
if (!spent)
|
|
156
|
+
return refuse('used');
|
|
157
|
+
const link = toConnectLink(spent);
|
|
158
|
+
audit({
|
|
159
|
+
action: 'consumeConnectLink',
|
|
160
|
+
target: targetOf(link),
|
|
161
|
+
before: { status: 'outstanding' },
|
|
162
|
+
after: { id: link.id, provider: link.provider, status: 'used', accountRef: link.accountRef },
|
|
163
|
+
});
|
|
164
|
+
return { ok: true, link };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The compensating half of consume: the callback spent the link, then failed to store the
|
|
168
|
+
* credential, so nothing was connected and the link goes back to outstanding — a platform
|
|
169
|
+
* hiccup costs a retry, not a fresh link mailed to a client. Only a `used` row moves (a
|
|
170
|
+
* revocation is never undone by this), and only an unexpired one: a link that lapsed while
|
|
171
|
+
* the store failed stays spent rather than coming back already dead.
|
|
172
|
+
*/
|
|
173
|
+
export function restoreConnectLinkRow(sql, key, now, audit) {
|
|
174
|
+
const restored = sql(`UPDATE _substrat_connect_links
|
|
175
|
+
SET status = 'outstanding', used_at = NULL, account_ref = NULL, account_label = NULL
|
|
176
|
+
WHERE id = ? AND tenant_id = ? AND scope_id = ? AND status = 'used' AND expires_at > ?
|
|
177
|
+
RETURNING ${COLUMNS}`, [key.id, key.tenantId, key.scopeId, now])[0];
|
|
178
|
+
if (!restored)
|
|
179
|
+
return undefined;
|
|
180
|
+
const link = toConnectLink(restored);
|
|
181
|
+
audit({
|
|
182
|
+
action: 'restoreConnectLink',
|
|
183
|
+
target: targetOf(link),
|
|
184
|
+
before: { status: 'used' },
|
|
185
|
+
after: { id: link.id, provider: link.provider, status: 'outstanding' },
|
|
186
|
+
});
|
|
187
|
+
return link;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=connect-links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-links.js","sourceRoot":"","sources":["../src/connect-links.ts"],"names":[],"mappings":"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"}
|
package/dist/connect-state.d.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* reason to have one — presses Connect on the vertical's own screen, and the vertical's
|
|
10
10
|
* operation is the permission-checked act.
|
|
11
11
|
*
|
|
12
|
-
* **Why
|
|
13
|
-
* and clicked days later by someone else, so it is a row: single-use, revocable, and
|
|
14
|
-
* when the minting admin loses access. A vertical-minted round is clicked in-session by
|
|
12
|
+
* **Why an in-session claim carries no link row.** The dashboard's connect link is minted,
|
|
13
|
+
* mailed, and clicked days later by someone else, so it is a row: single-use, revocable, and
|
|
14
|
+
* dead when the minting admin loses access. A vertical-minted round is clicked in-session by
|
|
15
15
|
* the person who just pressed the button, so the row would be state nobody reads. What
|
|
16
16
|
* replaces it is the expiry — minutes, not a week — and the account leg of the connection
|
|
17
17
|
* key: a replayed round re-consents the SAME company, which the store rotates in place
|
|
18
18
|
* rather than duplicating (#1267). The consent code itself is single-use at the provider.
|
|
19
|
+
* A vertical's MAILED link is the other case again, and carries a row: `linkId`, below.
|
|
19
20
|
*
|
|
20
21
|
* **Why the kernel.** Two workers hold the halves — the control plane mints (it owns the
|
|
21
22
|
* directory the vertical is re-derived from), the dashboard verifies (it owns the one
|
|
@@ -70,6 +71,14 @@ export interface ConnectStateClaim {
|
|
|
70
71
|
* just landed without waiting to match on an organisation number.
|
|
71
72
|
*/
|
|
72
73
|
subjectRef?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Set when the round is a MAILED link (connections.md §3.5.4): the platform-held row in
|
|
76
|
+
* `_substrat_connect_links` that makes it single-use and revocable. The signature alone
|
|
77
|
+
* cannot be withdrawn, and a link lives days — so the callback asks this row, consumes it
|
|
78
|
+
* before storing, and refuses a round whose row is spent or revoked. Absent: an in-session
|
|
79
|
+
* round, which lives minutes and has no row (above). `exp` is then the row's own expiry.
|
|
80
|
+
*/
|
|
81
|
+
linkId?: string;
|
|
73
82
|
/** Epoch ms. */
|
|
74
83
|
exp: number;
|
|
75
84
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-state.d.ts","sourceRoot":"","sources":["../src/connect-state.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"connect-state.d.ts","sourceRoot":"","sources":["../src/connect-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,8FAA8F;AAC9F,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,uCAAuC,CAAC;AAE1E;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,6GAA6G;IAC7G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AA6DD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAOxG;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA6BnC"}
|
package/dist/connect-state.js
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* reason to have one — presses Connect on the vertical's own screen, and the vertical's
|
|
10
10
|
* operation is the permission-checked act.
|
|
11
11
|
*
|
|
12
|
-
* **Why
|
|
13
|
-
* and clicked days later by someone else, so it is a row: single-use, revocable, and
|
|
14
|
-
* when the minting admin loses access. A vertical-minted round is clicked in-session by
|
|
12
|
+
* **Why an in-session claim carries no link row.** The dashboard's connect link is minted,
|
|
13
|
+
* mailed, and clicked days later by someone else, so it is a row: single-use, revocable, and
|
|
14
|
+
* dead when the minting admin loses access. A vertical-minted round is clicked in-session by
|
|
15
15
|
* the person who just pressed the button, so the row would be state nobody reads. What
|
|
16
16
|
* replaces it is the expiry — minutes, not a week — and the account leg of the connection
|
|
17
17
|
* key: a replayed round re-consents the SAME company, which the store rotates in place
|
|
18
18
|
* rather than duplicating (#1267). The consent code itself is single-use at the provider.
|
|
19
|
+
* A vertical's MAILED link is the other case again, and carries a row: `linkId`, below.
|
|
19
20
|
*
|
|
20
21
|
* **Why the kernel.** Two workers hold the halves — the control plane mints (it owns the
|
|
21
22
|
* directory the vertical is re-derived from), the dashboard verifies (it owns the one
|
|
@@ -114,6 +115,10 @@ export async function verifyConnectState(platformSecret, token, now) {
|
|
|
114
115
|
if (typeof claim[field] !== 'string' || !claim[field])
|
|
115
116
|
return null;
|
|
116
117
|
}
|
|
118
|
+
// A link round with a malformed row reference would read as an in-session round — the
|
|
119
|
+
// one kind the callback spends nothing for — so it is refused, never downgraded.
|
|
120
|
+
if (claim.linkId !== undefined && (typeof claim.linkId !== 'string' || !claim.linkId))
|
|
121
|
+
return null;
|
|
117
122
|
return claim;
|
|
118
123
|
}
|
|
119
124
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-state.js","sourceRoot":"","sources":["../src/connect-state.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"connect-state.js","sourceRoot":"","sources":["../src/connect-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One schema object the kernel derives from a declaration (#2090): the CREATE statement as the
|
|
3
|
+
* kernel emits it, and the table SQLite files it under (`sqlite_master.tbl_name`). The DDL that
|
|
4
|
+
* creates it is assembled from this, so what is checked and what is run cannot differ.
|
|
5
|
+
*/
|
|
6
|
+
export interface DerivedObject {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly type: 'table' | 'trigger' | 'index';
|
|
9
|
+
/** `sqlite_master.tbl_name`: the table a trigger or index is on; a table's own name. */
|
|
10
|
+
readonly table: string;
|
|
11
|
+
/** The CREATE statement, without its trailing `;` — as `sqlite_master.sql` stores it. */
|
|
12
|
+
readonly sql: string;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=derived-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-object.d.ts","sourceRoot":"","sources":["../src/derived-object.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAC7C,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yFAAyF;IACzF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-object.js","sourceRoot":"","sources":["../src/derived-object.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the kernel derived onto a module's tables, checked and put back (#2090).
|
|
3
|
+
*
|
|
4
|
+
* | Object | Is | When it is missing or not what the kernel emits |
|
|
5
|
+
* |---|---|---|
|
|
6
|
+
* | `_substrat_archived_at` / `_substrat_trashed_at` (#119) | state | fail closed: it cannot be invented again |
|
|
7
|
+
* | the `born` / `moved` archive/trash guard triggers (#119) | derived | put back |
|
|
8
|
+
* | the derived list indexes, partial per view on a stateful table (#811) | derived | put back |
|
|
9
|
+
| the purge sweep's partial index on a trashable table with a horizon (#119) | derived | put back |
|
|
10
|
+
* | the search index and the three triggers keeping it in step (#827) | derived | rebuilt from the rows |
|
|
11
|
+
*
|
|
12
|
+
* The search triggers are a privacy fact, not only a freshness one: subject erasure rewrites a
|
|
13
|
+
* row's text and relies on the update and delete triggers to take the old text out of the index.
|
|
14
|
+
* Without them, erased text stays searchable.
|
|
15
|
+
*
|
|
16
|
+
* **An object is judged by its definition, not its name.** `sqlite_master` holds each one's
|
|
17
|
+
* CREATE statement as it was run, and the kernel knows the statement it emits
|
|
18
|
+
* (`DerivedObject`), so a same-named trigger that does nothing, an index on the wrong columns or
|
|
19
|
+
* with the wrong partial predicate, and an object filed under another table are all "not
|
|
20
|
+
* there". The text is compared exactly: nothing but the kernel writes these objects, and a
|
|
21
|
+
* normalisation that forgave whitespace would forgive it inside a string literal too — a guard
|
|
22
|
+
* keyed on a different entity type. An object an older kernel worded differently is re-created
|
|
23
|
+
* once.
|
|
24
|
+
*
|
|
25
|
+
* Four places ask, and all four come here, so "what a table is owed" is written once:
|
|
26
|
+
*
|
|
27
|
+
* - **runtime DDL** (`ctx.sql`'s after-DDL hook, #1811) repairs inside the operation's
|
|
28
|
+
* transaction, whenever the scope has any derived plan. The text guard already refuses the DDL
|
|
29
|
+
* known to move a stateful table's rows; a search-only or list-only table has no such guard,
|
|
30
|
+
* and a module may rebuild it — this puts its triggers and indexes back before the operation
|
|
31
|
+
* commits. A lost state column fails the operation, and the DDL with it.
|
|
32
|
+
* - **a migration** checks the state columns after its own SQL, inside its own transaction, so a
|
|
33
|
+
* migration that drops one rolls back with nothing applied. The last migration of a pass then
|
|
34
|
+
* repairs the derived objects, inside that same transaction: SQLite's ordinary
|
|
35
|
+
* create-copy-rename rebuild of a table drops its triggers and indexes with it, and the
|
|
36
|
+
* migrations that made them are journaled, so they never run again. Only after the last,
|
|
37
|
+
* because a rebuild split over two migrations copies its rows in the second, and a guard put
|
|
38
|
+
* back in between would refuse the archived ones — which also means such a split must ship
|
|
39
|
+
* in ONE deploy: split over two, the guard is back before the copy runs.
|
|
40
|
+
* - **a wake with nothing pending** (and every `migrateScope`) repairs the same way, once per
|
|
41
|
+
* instance. That is what reaches a scope stripped before this existed, whose migrations are
|
|
42
|
+
* all journaled and will never run again.
|
|
43
|
+
* - **a restore** repairs after the rows, inside the load's transaction, which may legitimately
|
|
44
|
+
* bring them archived or trashed.
|
|
45
|
+
*
|
|
46
|
+
* ## What the journal says a table is owed
|
|
47
|
+
*
|
|
48
|
+
* Every expectation comes from `_substrat_migrations`, not only from the declarations: a scope
|
|
49
|
+
* mid-upgrade has declared a trash its column migration has not added yet, and a restored dump
|
|
50
|
+
* carries the journal of the scope it came from. A column is owed once the migration that added
|
|
51
|
+
* it is journaled; a derived object once the migration that derived it, for the CURRENT
|
|
52
|
+
* declaration, is. An object from a withdrawn declaration is not this module's business.
|
|
53
|
+
*/
|
|
54
|
+
import { SubstratError } from '@substrat-run/contracts';
|
|
55
|
+
import { type EntityStatePlan } from './entity-state.js';
|
|
56
|
+
import { type ListIndexPlan } from './list-index.js';
|
|
57
|
+
import type { ScopedSql } from './scope-host.js';
|
|
58
|
+
import { type SearchIndexPlan } from './search-index.js';
|
|
59
|
+
/** A scope's derivation inputs: every registered module's plans, entity type → plan. */
|
|
60
|
+
export interface DerivedPlans {
|
|
61
|
+
readonly state: ReadonlyMap<string, EntityStatePlan>;
|
|
62
|
+
readonly lists: ReadonlyMap<string, ListIndexPlan>;
|
|
63
|
+
readonly search: ReadonlyMap<string, SearchIndexPlan>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A table without a state column its journal says it has (`internal`). `migration` is the
|
|
67
|
+
* journaled `<module>@<version>` that added the column — what a scope failing closed on a wake,
|
|
68
|
+
* where no migration ran, records as its failure.
|
|
69
|
+
*/
|
|
70
|
+
export declare class StateColumnLost extends SubstratError {
|
|
71
|
+
readonly migration: string;
|
|
72
|
+
constructor(migration: string, message: string);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* After runtime DDL, inside the operation's transaction: the same repair a migration pass makes.
|
|
76
|
+
* A lost state column throws `internal`, and the operation and its DDL roll back.
|
|
77
|
+
*/
|
|
78
|
+
export declare function afterRuntimeDdl(sql: ScopedSql, run: (ddl: string) => void, plans: DerivedPlans): void;
|
|
79
|
+
/** Whether a scope has anything derived for `afterRuntimeDdl` to keep — the hook is installed only then. */
|
|
80
|
+
export declare const derivesAnything: (plans: DerivedPlans) => boolean;
|
|
81
|
+
/**
|
|
82
|
+
* A migration's half, after its own SQL and inside its own transaction: the state columns are
|
|
83
|
+
* still where the journal put them. A migration that dropped one throws here and rolls back —
|
|
84
|
+
* the rows keep their state, and the scope fails closed naming the migration.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertEntityStateColumns(sql: ScopedSql, plans: DerivedPlans, after: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Check the state columns, then re-create every derived set the journal says a table is owed
|
|
89
|
+
* and the catalogue does not hold exactly as emitted. Run inside the caller's transaction, so a
|
|
90
|
+
* failure — a lost column, or DDL that will not apply — rolls the caller back whole.
|
|
91
|
+
*
|
|
92
|
+
* `run` executes a multi-statement script (a Durable Object splits it first). `after` names what
|
|
93
|
+
* left the schema this way, for the message. `absentTable: 'skip'` is a restore's: a table the
|
|
94
|
+
* dump did not carry is left absent, and owed nothing. Everywhere else an owed table that is
|
|
95
|
+
* gone has lost its state, and fails closed.
|
|
96
|
+
*/
|
|
97
|
+
export declare function repairDerivedObjects(sql: ScopedSql, run: (ddl: string) => void, plans: DerivedPlans, opts: {
|
|
98
|
+
readonly after: string;
|
|
99
|
+
readonly absentTable?: 'fail' | 'skip';
|
|
100
|
+
}): void;
|
|
101
|
+
/**
|
|
102
|
+
* Both adapters' migration pass, after one migration's SQL and inside its transaction: the
|
|
103
|
+
* column check, and — after the last migration of the pass — the derived objects repaired.
|
|
104
|
+
* `key` is `<module>@<version>`, which a failure names. A last entry the pass skips (journaled
|
|
105
|
+
* by a pass that ran first) needs nothing here: that pass repaired them after it.
|
|
106
|
+
*/
|
|
107
|
+
export declare function afterMigration(sql: ScopedSql, run: (ddl: string) => void, plans: DerivedPlans, key: string, last: boolean): void;
|
|
108
|
+
//# sourceMappingURL=derived-objects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-objects.d.ts","sourceRoot":"","sources":["../src/derived-objects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExD,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAoD,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACvG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAA0D,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEjH,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACrD,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACvD;AAED;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,aAAa;IAE9C,QAAQ,CAAC,SAAS,EAAE,MAAM;IAD5B,YACW,SAAS,EAAE,MAAM,EAC1B,OAAO,EAAE,MAAM,EAGhB;CACF;AAsHD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAErG;AAED,4GAA4G;AAC5G,eAAO,MAAM,eAAe,UAAW,YAAY,KAAG,OACiB,CAAC;AAExE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjG;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC1B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACvE,IAAI,CAON;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC1B,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,OAAO,GACZ,IAAI,CAGN"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the kernel derived onto a module's tables, checked and put back (#2090).
|
|
3
|
+
*
|
|
4
|
+
* | Object | Is | When it is missing or not what the kernel emits |
|
|
5
|
+
* |---|---|---|
|
|
6
|
+
* | `_substrat_archived_at` / `_substrat_trashed_at` (#119) | state | fail closed: it cannot be invented again |
|
|
7
|
+
* | the `born` / `moved` archive/trash guard triggers (#119) | derived | put back |
|
|
8
|
+
* | the derived list indexes, partial per view on a stateful table (#811) | derived | put back |
|
|
9
|
+
| the purge sweep's partial index on a trashable table with a horizon (#119) | derived | put back |
|
|
10
|
+
* | the search index and the three triggers keeping it in step (#827) | derived | rebuilt from the rows |
|
|
11
|
+
*
|
|
12
|
+
* The search triggers are a privacy fact, not only a freshness one: subject erasure rewrites a
|
|
13
|
+
* row's text and relies on the update and delete triggers to take the old text out of the index.
|
|
14
|
+
* Without them, erased text stays searchable.
|
|
15
|
+
*
|
|
16
|
+
* **An object is judged by its definition, not its name.** `sqlite_master` holds each one's
|
|
17
|
+
* CREATE statement as it was run, and the kernel knows the statement it emits
|
|
18
|
+
* (`DerivedObject`), so a same-named trigger that does nothing, an index on the wrong columns or
|
|
19
|
+
* with the wrong partial predicate, and an object filed under another table are all "not
|
|
20
|
+
* there". The text is compared exactly: nothing but the kernel writes these objects, and a
|
|
21
|
+
* normalisation that forgave whitespace would forgive it inside a string literal too — a guard
|
|
22
|
+
* keyed on a different entity type. An object an older kernel worded differently is re-created
|
|
23
|
+
* once.
|
|
24
|
+
*
|
|
25
|
+
* Four places ask, and all four come here, so "what a table is owed" is written once:
|
|
26
|
+
*
|
|
27
|
+
* - **runtime DDL** (`ctx.sql`'s after-DDL hook, #1811) repairs inside the operation's
|
|
28
|
+
* transaction, whenever the scope has any derived plan. The text guard already refuses the DDL
|
|
29
|
+
* known to move a stateful table's rows; a search-only or list-only table has no such guard,
|
|
30
|
+
* and a module may rebuild it — this puts its triggers and indexes back before the operation
|
|
31
|
+
* commits. A lost state column fails the operation, and the DDL with it.
|
|
32
|
+
* - **a migration** checks the state columns after its own SQL, inside its own transaction, so a
|
|
33
|
+
* migration that drops one rolls back with nothing applied. The last migration of a pass then
|
|
34
|
+
* repairs the derived objects, inside that same transaction: SQLite's ordinary
|
|
35
|
+
* create-copy-rename rebuild of a table drops its triggers and indexes with it, and the
|
|
36
|
+
* migrations that made them are journaled, so they never run again. Only after the last,
|
|
37
|
+
* because a rebuild split over two migrations copies its rows in the second, and a guard put
|
|
38
|
+
* back in between would refuse the archived ones — which also means such a split must ship
|
|
39
|
+
* in ONE deploy: split over two, the guard is back before the copy runs.
|
|
40
|
+
* - **a wake with nothing pending** (and every `migrateScope`) repairs the same way, once per
|
|
41
|
+
* instance. That is what reaches a scope stripped before this existed, whose migrations are
|
|
42
|
+
* all journaled and will never run again.
|
|
43
|
+
* - **a restore** repairs after the rows, inside the load's transaction, which may legitimately
|
|
44
|
+
* bring them archived or trashed.
|
|
45
|
+
*
|
|
46
|
+
* ## What the journal says a table is owed
|
|
47
|
+
*
|
|
48
|
+
* Every expectation comes from `_substrat_migrations`, not only from the declarations: a scope
|
|
49
|
+
* mid-upgrade has declared a trash its column migration has not added yet, and a restored dump
|
|
50
|
+
* carries the journal of the scope it came from. A column is owed once the migration that added
|
|
51
|
+
* it is journaled; a derived object once the migration that derived it, for the CURRENT
|
|
52
|
+
* declaration, is. An object from a withdrawn declaration is not this module's business.
|
|
53
|
+
*/
|
|
54
|
+
import { SubstratError } from '@substrat-run/contracts';
|
|
55
|
+
import { entityStateGuardVersion, entityStateTriggerDdl, entityStateTriggerObjects, purgeIndexObjects, purgeIndexRepairDdl, purgeIndexVersion, stateColumnVersionsOf, } from './entity-state.js';
|
|
56
|
+
import { listIndexDdl, listIndexObjects, listIndexVersion } from './list-index.js';
|
|
57
|
+
import { searchIndexDdl, searchIndexObjects, searchIndexVersion } from './search-index.js';
|
|
58
|
+
/**
|
|
59
|
+
* A table without a state column its journal says it has (`internal`). `migration` is the
|
|
60
|
+
* journaled `<module>@<version>` that added the column — what a scope failing closed on a wake,
|
|
61
|
+
* where no migration ran, records as its failure.
|
|
62
|
+
*/
|
|
63
|
+
export class StateColumnLost extends SubstratError {
|
|
64
|
+
migration;
|
|
65
|
+
constructor(migration, message) {
|
|
66
|
+
super('internal', message);
|
|
67
|
+
this.migration = migration;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** `main`'s own catalogue by lowercased name — so a same-named temp object cannot stand in. */
|
|
71
|
+
function catalogueOf(sql) {
|
|
72
|
+
return new Map(sql
|
|
73
|
+
.query(`SELECT type, name, tbl_name, sql FROM main.sqlite_master`)
|
|
74
|
+
.map((r) => [r.name.toLowerCase(), { type: r.type, table: r.tbl_name, sql: r.sql }]));
|
|
75
|
+
}
|
|
76
|
+
/** The first object of `d` the catalogue does not hold exactly as the kernel emits it. */
|
|
77
|
+
const wrongIn = (catalogue, d) => d.objects.find((o) => {
|
|
78
|
+
const found = catalogue.get(o.name.toLowerCase());
|
|
79
|
+
return (found?.type !== o.type ||
|
|
80
|
+
found.table.toLowerCase() !== o.table.toLowerCase() ||
|
|
81
|
+
found.sql !== o.sql);
|
|
82
|
+
});
|
|
83
|
+
const columnsOn = (sql, table) => new Set(sql.query(`SELECT name FROM pragma_table_info(?, 'main')`, [table]).map((r) => r.name.toLowerCase()));
|
|
84
|
+
function expectations(sql, plans) {
|
|
85
|
+
const journal = new Set(sql.query(`SELECT module_id, version FROM _substrat_migrations`).map((r) => `${r.module_id}@${r.version}`));
|
|
86
|
+
const applied = (moduleId, version) => journal.has(`${moduleId}@${version}`);
|
|
87
|
+
const byTable = new Map();
|
|
88
|
+
const entry = (table) => {
|
|
89
|
+
const key = table.toLowerCase();
|
|
90
|
+
if (!byTable.has(key))
|
|
91
|
+
byTable.set(key, { table, derived: [] });
|
|
92
|
+
return byTable.get(key);
|
|
93
|
+
};
|
|
94
|
+
for (const plan of plans.state.values()) {
|
|
95
|
+
const e = entry(plan.table);
|
|
96
|
+
const columns = stateColumnVersionsOf(plan)
|
|
97
|
+
.filter((c) => applied(plan.moduleId, c.version))
|
|
98
|
+
.map((c) => ({ column: c.column, migration: `${plan.moduleId}@${c.version}` }));
|
|
99
|
+
if (columns.length)
|
|
100
|
+
e.columns = columns;
|
|
101
|
+
if (applied(plan.moduleId, entityStateGuardVersion(plan))) {
|
|
102
|
+
e.derived.push({ objects: entityStateTriggerObjects(plan), ddl: entityStateTriggerDdl(plan) });
|
|
103
|
+
}
|
|
104
|
+
const purge = purgeIndexObjects(plan);
|
|
105
|
+
if (purge.length && applied(plan.moduleId, purgeIndexVersion(plan))) {
|
|
106
|
+
e.derived.push({ objects: purge, ddl: purgeIndexRepairDdl(plan) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const plan of plans.lists.values()) {
|
|
110
|
+
const objects = listIndexObjects(plan);
|
|
111
|
+
if (objects.length && applied(plan.moduleId, listIndexVersion(plan))) {
|
|
112
|
+
entry(plan.table).derived.push({ objects, ddl: listIndexDdl(plan) });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// The DDL ends in a full `rebuild` from the content table, which is also what repairs an index
|
|
116
|
+
// that went stale while its triggers were gone or wrong.
|
|
117
|
+
for (const plan of plans.search.values()) {
|
|
118
|
+
if (applied(plan.moduleId, searchIndexVersion(plan))) {
|
|
119
|
+
entry(plan.table).derived.push({ objects: searchIndexObjects(plan), ddl: searchIndexDdl(plan) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return [...byTable.values()];
|
|
123
|
+
}
|
|
124
|
+
const lost = (after, table, what, migration) => {
|
|
125
|
+
throw new StateColumnLost(migration, `${after} left '${table.table}' without ${what} — its archive/trash guarantees depend on it; nothing was changed`);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The state half: the table holds every column its journal added. Throws `StateColumnLost` on
|
|
129
|
+
* the first one gone. An absent table counts as losing them all, unless `absentTable` is
|
|
130
|
+
* `'skip'` — a restore's case, where a dump that did not carry a table is not made to invent one.
|
|
131
|
+
*/
|
|
132
|
+
function assertColumns(sql, catalogue, table, after, absentTable = 'fail') {
|
|
133
|
+
if (!table.columns)
|
|
134
|
+
return;
|
|
135
|
+
if (catalogue.get(table.table.toLowerCase())?.type !== 'table') {
|
|
136
|
+
if (absentTable === 'skip')
|
|
137
|
+
return;
|
|
138
|
+
lost(after, table, 'its table', table.columns[0].migration);
|
|
139
|
+
}
|
|
140
|
+
const present = columnsOn(sql, table.table);
|
|
141
|
+
for (const c of table.columns)
|
|
142
|
+
if (!present.has(c.column))
|
|
143
|
+
lost(after, table, c.column, c.migration);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* After runtime DDL, inside the operation's transaction: the same repair a migration pass makes.
|
|
147
|
+
* A lost state column throws `internal`, and the operation and its DDL roll back.
|
|
148
|
+
*/
|
|
149
|
+
export function afterRuntimeDdl(sql, run, plans) {
|
|
150
|
+
repairDerivedObjects(sql, run, plans, { after: 'runtime DDL' });
|
|
151
|
+
}
|
|
152
|
+
/** Whether a scope has anything derived for `afterRuntimeDdl` to keep — the hook is installed only then. */
|
|
153
|
+
export const derivesAnything = (plans) => plans.state.size > 0 || plans.lists.size > 0 || plans.search.size > 0;
|
|
154
|
+
/**
|
|
155
|
+
* A migration's half, after its own SQL and inside its own transaction: the state columns are
|
|
156
|
+
* still where the journal put them. A migration that dropped one throws here and rolls back —
|
|
157
|
+
* the rows keep their state, and the scope fails closed naming the migration.
|
|
158
|
+
*/
|
|
159
|
+
export function assertEntityStateColumns(sql, plans, after) {
|
|
160
|
+
if (plans.state.size === 0)
|
|
161
|
+
return; // most scopes: nothing holds state, so no read at all
|
|
162
|
+
const catalogue = catalogueOf(sql);
|
|
163
|
+
for (const table of expectations(sql, plans))
|
|
164
|
+
assertColumns(sql, catalogue, table, after);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check the state columns, then re-create every derived set the journal says a table is owed
|
|
168
|
+
* and the catalogue does not hold exactly as emitted. Run inside the caller's transaction, so a
|
|
169
|
+
* failure — a lost column, or DDL that will not apply — rolls the caller back whole.
|
|
170
|
+
*
|
|
171
|
+
* `run` executes a multi-statement script (a Durable Object splits it first). `after` names what
|
|
172
|
+
* left the schema this way, for the message. `absentTable: 'skip'` is a restore's: a table the
|
|
173
|
+
* dump did not carry is left absent, and owed nothing. Everywhere else an owed table that is
|
|
174
|
+
* gone has lost its state, and fails closed.
|
|
175
|
+
*/
|
|
176
|
+
export function repairDerivedObjects(sql, run, plans, opts) {
|
|
177
|
+
const catalogue = catalogueOf(sql);
|
|
178
|
+
for (const table of expectations(sql, plans)) {
|
|
179
|
+
assertColumns(sql, catalogue, table, opts.after, opts.absentTable);
|
|
180
|
+
if (catalogue.get(table.table.toLowerCase())?.type !== 'table')
|
|
181
|
+
continue;
|
|
182
|
+
for (const d of table.derived)
|
|
183
|
+
if (wrongIn(catalogue, d))
|
|
184
|
+
run(d.ddl);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Both adapters' migration pass, after one migration's SQL and inside its transaction: the
|
|
189
|
+
* column check, and — after the last migration of the pass — the derived objects repaired.
|
|
190
|
+
* `key` is `<module>@<version>`, which a failure names. A last entry the pass skips (journaled
|
|
191
|
+
* by a pass that ran first) needs nothing here: that pass repaired them after it.
|
|
192
|
+
*/
|
|
193
|
+
export function afterMigration(sql, run, plans, key, last) {
|
|
194
|
+
if (last)
|
|
195
|
+
repairDerivedObjects(sql, run, plans, { after: `migration ${key}` });
|
|
196
|
+
else
|
|
197
|
+
assertEntityStateColumns(sql, plans, `migration ${key}`);
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=derived-objects.js.map
|