@serfab/cadre-core 1.15.0 → 1.17.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/README.md +1 -1
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +55 -138
- package/dist/cadre-invite-protocol.js +94 -449
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +530 -247
- package/dist/cadre-node.js +1046 -525
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +22 -23
- package/dist/control-cohort.js +24 -24
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +416 -136
- package/dist/control-database.js +757 -378
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +5 -4
- package/dist/control-protocol-guard.js +6 -3
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +80 -0
- package/dist/control-schema-version.js +203 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +254 -70
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -5
- package/dist/control-write-retry.js +10 -8
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +3 -3
- package/dist/joined-strand-store.js +3 -3
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +250 -0
- package/dist/member-redemption.js +485 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +247 -0
- package/dist/owner-grant-protocol.js +330 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +131 -109
- package/dist/seed-bootstrap.js +260 -248
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +1 -0
- package/dist/strand-database.js +3 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +4 -3
- package/dist/strand-first-sync-gate.js +4 -3
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-membership-writer.d.ts +13 -2
- package/dist/strand-membership-writer.js +52 -16
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +31 -30
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/types.d.ts +286 -103
- package/dist/types.js.map +1 -1
- package/package.json +16 -7
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +131 -531
- package/src/cadre-join-request.ts +188 -167
- package/src/cadre-node.ts +1166 -541
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +127 -128
- package/src/control-database.ts +940 -456
- package/src/control-protocol-guard.ts +7 -4
- package/src/control-schema-version.ts +251 -0
- package/src/control-schema.ts +265 -70
- package/src/control-write-retry.ts +569 -567
- package/src/delegate-admission.ts +37 -0
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/index.ts +102 -5
- package/src/joined-strand-store.ts +4 -4
- package/src/key-store.ts +10 -4
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +642 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +532 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-authorization.ts +558 -488
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +301 -312
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +3 -0
- package/src/strand-first-sync-gate.ts +4 -3
- package/src/strand-instance-manager.ts +1 -1
- package/src/strand-membership-writer.ts +61 -16
- package/src/strand-move.ts +45 -36
- package/src/strand-peer-redial.ts +2 -3
- package/src/types.ts +304 -103
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## The classes
|
|
16
16
|
*
|
|
17
|
-
* - `stranger-open` — seed delivery, strand formation, cadre-invitation redemption
|
|
18
|
-
* each handler makes its own trust decision in-protocol (a seed
|
|
19
|
-
* anchored trust policy, a formation token, an invitation's
|
|
17
|
+
* - `stranger-open` — seed delivery, strand formation, cadre-invitation redemption, owner-grant
|
|
18
|
+
* redemption. Any peer; each handler makes its own trust decision in-protocol (a seed
|
|
19
|
+
* signature against the anchored trust policy, a formation token, an invitation's or a
|
|
20
|
+
* grant's possession proof).
|
|
20
21
|
* - `transport` — libp2p plumbing (identify, ping, hole punching, AutoNAT, the circuit-relay
|
|
21
22
|
* stop and hop protocols, WebRTC signaling). Every connection runs these, the redeeming
|
|
22
23
|
* device's own included: refusing identify would make that device treat the member as
|
|
@@ -81,6 +82,7 @@ import type { Connection, Libp2p, Stream, StreamHandler, StreamHandlerRecord } f
|
|
|
81
82
|
import { SEED_PROTOCOL } from './seed-bootstrap.js';
|
|
82
83
|
import { FORMATION_PROTOCOL } from './strand-formation-protocol.js';
|
|
83
84
|
import { CADRE_INVITE_PROTOCOL } from './cadre-invite-protocol.js';
|
|
85
|
+
import { OWNER_GRANT_PROTOCOL } from './owner-grant-protocol.js';
|
|
84
86
|
import { WAKE_PROTOCOL } from './strand-wake-protocol.js';
|
|
85
87
|
import { STRAND_ADDR_PROTOCOL } from './strand-addr-protocol.js';
|
|
86
88
|
import { withTimeout } from './control-stream.js';
|
|
@@ -89,7 +91,7 @@ const log = debug('sereus:cadre:control-protocol-guard');
|
|
|
89
91
|
|
|
90
92
|
/** What a peer must be to open a stream on a control-node protocol (see the module doc). */
|
|
91
93
|
export type ControlProtocolClass =
|
|
92
|
-
/** Any peer. The handler makes its own trust decision in-protocol (seed, formation, cadre-invite). */
|
|
94
|
+
/** Any peer. The handler makes its own trust decision in-protocol (seed, formation, cadre-invite, owner-grant). */
|
|
93
95
|
| 'stranger-open'
|
|
94
96
|
/** libp2p plumbing that a stranger's connection needs or that carries no party data. */
|
|
95
97
|
| 'transport'
|
|
@@ -145,6 +147,7 @@ export function controlProtocolClasses(partyId: string): ReadonlyMap<string, Con
|
|
|
145
147
|
[SEED_PROTOCOL, 'stranger-open'],
|
|
146
148
|
[FORMATION_PROTOCOL, 'stranger-open'],
|
|
147
149
|
[CADRE_INVITE_PROTOCOL, 'stranger-open'],
|
|
150
|
+
[OWNER_GRANT_PROTOCOL, 'stranger-open'],
|
|
148
151
|
|
|
149
152
|
[WAKE_PROTOCOL, 'members'],
|
|
150
153
|
[STRAND_ADDR_PROTOCOL, 'members'],
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* control-schema-version.ts — the check `ControlDatabase.loadSchema` runs on a warm control
|
|
3
|
+
* store after declaring the control schema and before applying it.
|
|
4
|
+
*
|
|
5
|
+
* `apply schema CadreControl` runs whatever DDL makes the hydrated catalog equal the declared
|
|
6
|
+
* schema. Against a store written by another release that DDL is either refused by the
|
|
7
|
+
* optimystic module part-way through (`alter table … drop constraint`, `add column`,
|
|
8
|
+
* `drop column`), which fails the start with a message naming whichever step came first, or
|
|
9
|
+
* runs and destroys rows (`drop table`). This module decides from the pending DDL and the
|
|
10
|
+
* stored schema version, before any of it runs, and throws one typed error instead.
|
|
11
|
+
*
|
|
12
|
+
* The same change classifier ({@link classifyControlSchemaChange}) is what
|
|
13
|
+
* `control-schema-versions.spec.ts` runs over each pair of consecutive control schema versions,
|
|
14
|
+
* so a schema edit that no store could be migrated across fails when it is made. Where the
|
|
15
|
+
* version lives, when it changes and what a new version may change: docs/architecture.md →
|
|
16
|
+
* "Control schema changes and incompatible stores".
|
|
17
|
+
*
|
|
18
|
+
* NOTE: the stored tag is the version the store's `OwnerKey` table was CREATED under. The
|
|
19
|
+
* optimystic plugin does not persist `alter table … set tags` (Quereus applies a tag change to
|
|
20
|
+
* its catalog without calling the module, and the plugin follows it in memory only), so the
|
|
21
|
+
* `set tags` an additive upgrade runs raises the version for that process alone. The check stays
|
|
22
|
+
* safe regardless, because it lets through only DDL that adds, so an older build's diff against
|
|
23
|
+
* an upgraded store lists the drop and is refused on that ground. Migrating a store in place
|
|
24
|
+
* (control-store-migrates-on-start) needs the version a store was upgraded to, so it needs that
|
|
25
|
+
* persisted upstream (optimystic-catalog-only-alter-table).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Parser, type Database, type SchemaManager, type SqlValue, type TableSchema } from '@quereus/quereus';
|
|
29
|
+
import type { AlterTableStmt, ColumnDef, Statement } from '@quereus/quereus/parser';
|
|
30
|
+
import { CONTROL_SCHEMA_NAME, CONTROL_SCHEMA_VERSION_TAG } from './control-schema.js';
|
|
31
|
+
import { controlStorageScope } from './storage-scope.js';
|
|
32
|
+
|
|
33
|
+
/** The control table whose tags carry {@link CONTROL_SCHEMA_VERSION_TAG}. */
|
|
34
|
+
const VERSION_TABLE = 'OwnerKey';
|
|
35
|
+
|
|
36
|
+
/** A warm control store this build cannot apply its control schema to without destroying data. */
|
|
37
|
+
export class ControlStoreIncompatibleError extends Error {
|
|
38
|
+
readonly partyId: string;
|
|
39
|
+
/** The version the store records: the one it was created under; 0 = before versions were recorded. */
|
|
40
|
+
readonly storeVersion: number;
|
|
41
|
+
/** The version this build declares. */
|
|
42
|
+
readonly buildVersion: number;
|
|
43
|
+
/** The DDL the apply would have run, from `diff schema`; for diagnostics, not for parsing. */
|
|
44
|
+
readonly pendingDdl: readonly string[];
|
|
45
|
+
/** `controlStorageScope(partyId)`: the storage the embedder deletes to recreate (docs/architecture.md). */
|
|
46
|
+
readonly storageScope: string;
|
|
47
|
+
|
|
48
|
+
constructor(partyId: string, storeVersion: number, buildVersion: number, pendingDdl: readonly string[]) {
|
|
49
|
+
const storageScope = controlStorageScope(partyId);
|
|
50
|
+
super(
|
|
51
|
+
`The control store for party ${JSON.stringify(partyId)} ${provenance(storeVersion, buildVersion)} ` +
|
|
52
|
+
`(it records control schema version ${storeVersion}; this build declares ${buildVersion}), and this build ` +
|
|
53
|
+
'cannot apply its control schema to it without losing data. Run a release whose control schema ' +
|
|
54
|
+
`matches the store, or recreate the control store (storage scope ${storageScope}) as ` +
|
|
55
|
+
'docs/architecture.md describes.'
|
|
56
|
+
);
|
|
57
|
+
this.name = 'ControlStoreIncompatibleError';
|
|
58
|
+
this.partyId = partyId;
|
|
59
|
+
this.storeVersion = storeVersion;
|
|
60
|
+
this.buildVersion = buildVersion;
|
|
61
|
+
this.pendingDdl = pendingDdl;
|
|
62
|
+
this.storageScope = storageScope;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Which release created the store, relative to this build, for the error message. Created, not
|
|
68
|
+
* last written: the stored version does not move on an additive upgrade (module NOTE).
|
|
69
|
+
*/
|
|
70
|
+
function provenance(storeVersion: number, buildVersion: number): string {
|
|
71
|
+
if (storeVersion > buildVersion) {
|
|
72
|
+
return 'was created by a newer release';
|
|
73
|
+
}
|
|
74
|
+
return storeVersion < buildVersion
|
|
75
|
+
? 'was created by an older release'
|
|
76
|
+
: 'was created under the same control schema version';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Throw {@link ControlStoreIncompatibleError} when applying the declared control schema to the
|
|
81
|
+
* hydrated store would destroy data; return having run nothing otherwise. Needs
|
|
82
|
+
* `declare schema CadreControl` to have run, and runs only `diff schema`, which executes no DDL.
|
|
83
|
+
*
|
|
84
|
+
* A cold store (the hydrated catalog lists no control table) passes without a diff: the apply
|
|
85
|
+
* creates everything. A warm store is refused when either
|
|
86
|
+
* - any pending statement is not `additive` ({@link classifyControlSchemaChange}): every other
|
|
87
|
+
* change to an existing control table is refused by the optimystic module or discards rows; or
|
|
88
|
+
* - its version is above the build's: a newer release created it, so even a diff that only adds
|
|
89
|
+
* (the newer release no longer declares something this build does) would put back what that
|
|
90
|
+
* release removed, and the newer release would then refuse its own store.
|
|
91
|
+
*
|
|
92
|
+
* So a store from an older release still warm-starts when the only change since is a new table
|
|
93
|
+
* or index.
|
|
94
|
+
*/
|
|
95
|
+
export async function refuseIncompatibleControlStore(db: Database, partyId: string): Promise<void> {
|
|
96
|
+
if (!isWarmControlStore(db)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const storeVersion = schemaVersionOf(db.schemaManager.getTable(CONTROL_SCHEMA_NAME, VERSION_TABLE)?.tags);
|
|
100
|
+
const buildVersion = declaredSchemaVersion(db);
|
|
101
|
+
const pending = await pendingControlSchemaChanges(db);
|
|
102
|
+
// `catalog-only` changes stay refused until the optimystic module applies them
|
|
103
|
+
// (optimystic-catalog-only-alter-table); control-store-migrates-on-start widens this then.
|
|
104
|
+
if (storeVersion > buildVersion || pending.some((change) => change.kind !== 'additive')) {
|
|
105
|
+
throw new ControlStoreIncompatibleError(partyId, storeVersion, buildVersion, pending.map((change) => change.ddl));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether the hydrated catalog lists at least one control table, i.e. the store was written before. */
|
|
110
|
+
function isWarmControlStore(db: Database): boolean {
|
|
111
|
+
const schema = db.schemaManager.getSchema(CONTROL_SCHEMA_NAME);
|
|
112
|
+
return schema !== undefined && !schema.getAllTables().next().done;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The version the declared control schema carries on its version table; 0 when it carries none. */
|
|
116
|
+
export function declaredSchemaVersion(db: Database): number {
|
|
117
|
+
const declared = db.declaredSchemaManager.getDeclaredSchema(CONTROL_SCHEMA_NAME);
|
|
118
|
+
const table = declared?.items.find(
|
|
119
|
+
(item) => item.type === 'declaredTable' && item.tableStmt.table.name.toLowerCase() === VERSION_TABLE.toLowerCase()
|
|
120
|
+
);
|
|
121
|
+
return schemaVersionOf(table?.type === 'declaredTable' ? table.tableStmt.tags : undefined);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The control schema version a table's tags record: 0 when the tag is absent, which is what
|
|
126
|
+
* every store written before versions were recorded holds.
|
|
127
|
+
*/
|
|
128
|
+
function schemaVersionOf(tags: Readonly<Record<string, SqlValue>> | undefined): number {
|
|
129
|
+
const value = tags?.[CONTROL_SCHEMA_VERSION_TAG];
|
|
130
|
+
if (value === undefined || value === null) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const version = typeof value === 'bigint' ? Number(value) : value;
|
|
134
|
+
if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) {
|
|
135
|
+
throw new Error(`The ${VERSION_TABLE} tag ${CONTROL_SCHEMA_VERSION_TAG} holds ${String(value)}, not a control schema version`);
|
|
136
|
+
}
|
|
137
|
+
return version;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* How applying one `diff schema` statement to a warm store treats its rows, from most to least
|
|
142
|
+
* migratable (docs/architecture.md → "Control schema changes and incompatible stores"):
|
|
143
|
+
* - `additive`: a new table, a new index, or a table tag change, which the optimystic module
|
|
144
|
+
* applies today without touching a row;
|
|
145
|
+
* - `catalog-only`: a change the optimystic module applies to the table's catalog record without
|
|
146
|
+
* rewriting a row, once optimystic-catalog-only-alter-table ships;
|
|
147
|
+
* - `unmigratable`: everything else, which rewrites or discards rows.
|
|
148
|
+
*/
|
|
149
|
+
export type ControlSchemaChangeKind = 'additive' | 'catalog-only' | 'unmigratable';
|
|
150
|
+
|
|
151
|
+
const CHANGE_KINDS: readonly ControlSchemaChangeKind[] = ['additive', 'catalog-only', 'unmigratable'];
|
|
152
|
+
|
|
153
|
+
/** One row of `diff schema CadreControl`, classified. */
|
|
154
|
+
export interface PendingControlSchemaChange {
|
|
155
|
+
readonly ddl: string;
|
|
156
|
+
/** The least migratable kind among the row's statements. */
|
|
157
|
+
readonly kind: ControlSchemaChangeKind;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The DDL `apply schema CadreControl` would run now, one entry per `diff schema` row, each
|
|
162
|
+
* classified against the catalog `db` holds. Needs `declare schema CadreControl` to have run.
|
|
163
|
+
*/
|
|
164
|
+
export async function pendingControlSchemaChanges(db: Database): Promise<PendingControlSchemaChange[]> {
|
|
165
|
+
const changes: PendingControlSchemaChange[] = [];
|
|
166
|
+
for await (const row of db.eval(`diff schema ${CONTROL_SCHEMA_NAME}`)) {
|
|
167
|
+
const ddl = row.ddl as string;
|
|
168
|
+
const kinds = new Parser().parseAll(ddl).map((statement) => classifyControlSchemaChange(statement, db.schemaManager));
|
|
169
|
+
changes.push({ ddl, kind: leastMigratable(kinds) });
|
|
170
|
+
}
|
|
171
|
+
return changes;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The least migratable of `kinds`; `additive` when there are none. */
|
|
175
|
+
function leastMigratable(kinds: readonly ControlSchemaChangeKind[]): ControlSchemaChangeKind {
|
|
176
|
+
return CHANGE_KINDS[Math.max(0, ...kinds.map((kind) => CHANGE_KINDS.indexOf(kind)))];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The {@link ControlSchemaChangeKind} of one statement `diff schema` emits. A name the statement
|
|
181
|
+
* drops is resolved against `catalog`, the one the statement would run against: the hydrated
|
|
182
|
+
* store's at runtime.
|
|
183
|
+
*/
|
|
184
|
+
function classifyControlSchemaChange(statement: Statement, catalog: SchemaManager): ControlSchemaChangeKind {
|
|
185
|
+
switch (statement.type) {
|
|
186
|
+
case 'createTable':
|
|
187
|
+
return 'additive';
|
|
188
|
+
// NOTE: a `unique index` over an existing table fails the apply on a store whose rows
|
|
189
|
+
// already repeat the key, which the spec's empty tables cannot show. If a version adds
|
|
190
|
+
// one to a table that holds rows, prove the stored rows are unique on it.
|
|
191
|
+
case 'createIndex':
|
|
192
|
+
return 'additive';
|
|
193
|
+
case 'drop':
|
|
194
|
+
return statement.objectType === 'index' ? 'catalog-only' : 'unmigratable';
|
|
195
|
+
case 'alterTable':
|
|
196
|
+
return classifyTableAlteration(statement, catalog);
|
|
197
|
+
default:
|
|
198
|
+
return 'unmigratable';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function classifyTableAlteration({ table, action }: AlterTableStmt, catalog: SchemaManager): ControlSchemaChangeKind {
|
|
203
|
+
const stored = (): TableSchema | undefined => catalog.getTable(table.schema ?? CONTROL_SCHEMA_NAME, table.name);
|
|
204
|
+
switch (action.type) {
|
|
205
|
+
case 'setTags':
|
|
206
|
+
return 'additive';
|
|
207
|
+
// NOTE: Quereus checks stored rows against an added CHECK, so one that a stored row fails
|
|
208
|
+
// fails the apply on that store; control-schema-versions.spec.ts diffs empty tables and
|
|
209
|
+
// cannot see it. If a version adds a CHECK over existing columns, prove the rows hold it.
|
|
210
|
+
case 'addConstraint':
|
|
211
|
+
return action.constraint.type === 'check' ? 'catalog-only' : 'unmigratable';
|
|
212
|
+
case 'dropConstraint':
|
|
213
|
+
return hasCheckConstraint(stored(), action.name) ? 'catalog-only' : 'unmigratable';
|
|
214
|
+
case 'addColumn':
|
|
215
|
+
return isPlainNullableColumn(action.column) ? 'catalog-only' : 'unmigratable';
|
|
216
|
+
case 'dropColumn':
|
|
217
|
+
return isUnkeyedColumn(stored(), action.name) ? 'catalog-only' : 'unmigratable';
|
|
218
|
+
default:
|
|
219
|
+
return 'unmigratable';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function hasCheckConstraint(table: TableSchema | undefined, name: string): boolean {
|
|
224
|
+
return table?.checkConstraints.some((check) => check.name?.toLowerCase() === name.toLowerCase()) ?? false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Whether existing rows can read null for the column: declared `null` and nothing else (no
|
|
229
|
+
* default, generated expression or other column constraint). The `null` must be explicit,
|
|
230
|
+
* because Quereus makes a column not null unless it says otherwise.
|
|
231
|
+
*/
|
|
232
|
+
function isPlainNullableColumn(column: ColumnDef): boolean {
|
|
233
|
+
return column.constraints.length > 0 && column.constraints.every((constraint) => constraint.type === 'null');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Whether the table has the column and no index, unique constraint or primary key covers it.
|
|
238
|
+
*
|
|
239
|
+
* NOTE: judged against the catalog before the apply, so a column whose covering index the same
|
|
240
|
+
* diff drops (`diff schema` emits the `drop index` first) still counts as covered. Conservative;
|
|
241
|
+
* if a version needs that change, judge the diff as a sequence.
|
|
242
|
+
*/
|
|
243
|
+
function isUnkeyedColumn(table: TableSchema | undefined, name: string): boolean {
|
|
244
|
+
const column = table?.columnIndexMap.get(name.toLowerCase());
|
|
245
|
+
if (table === undefined || column === undefined) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return !table.primaryKeyDefinition.some((key) => key.index === column)
|
|
249
|
+
&& !(table.indexes ?? []).some((index) => index.columns.some((indexed) => indexed.index === column))
|
|
250
|
+
&& !(table.uniqueConstraints ?? []).some((unique) => unique.columns.includes(column));
|
|
251
|
+
}
|