@delali/sirannon-db 0.1.7 → 0.2.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 +117 -969
- package/dist/backup-scheduler/index.d.ts +1 -1
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/baseline-Br77Fnhb.d.ts +6 -0
- package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
- package/dist/{chunk-DJLX6CAE.mjs → chunk-4ISB7XMA.mjs} +2 -1
- package/dist/chunk-5NOIGN5Y.mjs +18 -0
- package/dist/chunk-67M7KAH6.mjs +90 -0
- package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
- package/dist/{chunk-4IGMIJQK.mjs → chunk-EBJXPQQO.mjs} +59 -20
- package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
- package/dist/chunk-GVCNMPOS.mjs +130 -0
- package/dist/chunk-H237TXZW.mjs +1407 -0
- package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
- package/dist/chunk-HR5CWTLC.mjs +661 -0
- package/dist/chunk-JU64Y7HM.mjs +299 -0
- package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
- package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
- package/dist/chunk-LNY2VVHE.mjs +18 -0
- package/dist/chunk-NVQS53NT.mjs +665 -0
- package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
- package/dist/chunk-OQVZBEBY.mjs +283 -0
- package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
- package/dist/chunk-TJF5GZSV.mjs +187 -0
- package/dist/chunk-TUD5CJ76.mjs +126 -0
- package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
- package/dist/client/index.d.ts +216 -321
- package/dist/client/index.mjs +1192 -1342
- package/dist/client/topology.d.ts +61 -0
- package/dist/client/topology.mjs +682 -0
- package/dist/client-base-CLWmH5Ln.d.ts +234 -0
- package/dist/codegen/cli.d.ts +2 -0
- package/dist/codegen/cli.mjs +11 -0
- package/dist/codegen/index.d.ts +30 -0
- package/dist/codegen/index.mjs +5 -0
- package/dist/core/index.d.ts +37 -16
- package/dist/core/index.mjs +2322 -298
- package/dist/core/writer-worker.mjs +28 -6
- package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +6 -6
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +6 -6
- package/dist/driver/wa-sqlite.d.ts +1 -136
- package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
- package/dist/file-migrations/index.d.ts +7 -5
- package/dist/file-migrations/index.mjs +42 -37
- package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
- package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
- package/dist/protocol-6KrSq2Hy.d.ts +66 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.mjs +137 -0
- package/dist/replication/coordinator/etcd.d.ts +5 -3
- package/dist/replication/coordinator/etcd.mjs +554 -462
- package/dist/replication/index.d.ts +51 -105
- package/dist/replication/index.mjs +1415 -2061
- package/dist/server/index.d.ts +111 -87
- package/dist/server/index.mjs +1917 -489
- package/dist/server-options-1JHu8pid.d.ts +191 -0
- package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
- package/dist/transport/grpc.d.ts +7 -4
- package/dist/transport/grpc.mjs +191 -97
- package/dist/transport/memory.d.ts +5 -134
- package/dist/transport/memory.mjs +3 -1
- package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
- package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
- package/dist/types-CL6piSnD.d.ts +43 -0
- package/dist/types-C_D8IhpO.d.ts +60 -0
- package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
- package/package.json +44 -6
- package/dist/chunk-TKGHYWQ6.mjs +0 -35
- package/dist/types-D4p4UyDK.d.ts +0 -26
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReplicationError } from './chunk-5NOIGN5Y.mjs';
|
|
2
2
|
|
|
3
3
|
// src/replication/errors.ts
|
|
4
|
-
var ReplicationError = class extends SirannonError {
|
|
5
|
-
constructor(message, code = "REPLICATION_ERROR", details) {
|
|
6
|
-
super(message, code);
|
|
7
|
-
this.details = details;
|
|
8
|
-
this.name = "ReplicationError";
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
4
|
var ConflictError = class extends ReplicationError {
|
|
12
5
|
constructor(message, table, rowId) {
|
|
13
6
|
super(message, "CONFLICT_ERROR");
|
|
@@ -22,12 +15,6 @@ var TransportError = class extends ReplicationError {
|
|
|
22
15
|
this.name = "TransportError";
|
|
23
16
|
}
|
|
24
17
|
};
|
|
25
|
-
var BatchValidationError = class extends ReplicationError {
|
|
26
|
-
constructor(message) {
|
|
27
|
-
super(message, "BATCH_VALIDATION_ERROR");
|
|
28
|
-
this.name = "BatchValidationError";
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
18
|
var WriteConcernError = class extends ReplicationError {
|
|
32
19
|
constructor(message) {
|
|
33
20
|
super(message, "WRITE_CONCERN_ERROR");
|
|
@@ -108,4 +95,4 @@ var SyncError = class extends ReplicationError {
|
|
|
108
95
|
}
|
|
109
96
|
};
|
|
110
97
|
|
|
111
|
-
export { AuthorityError,
|
|
98
|
+
export { AuthorityError, ConflictError, CoordinatorError, FailoverError, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, ProtocolVersionMismatchError, ReadConcernError, StalePrimaryError, SyncError, TopologyError, TransportError, UnsafeRecoveryRequiredError, WriteConcernError };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { IDENTIFIER_RE, selectCountTableRows, computeChecksum, canonicaliseForChecksum, selectUserSchemaObjects, validateDdlSafety, selectUserTableNames, referencedTables } from './chunk-NVQS53NT.mjs';
|
|
2
|
+
import { ReplicationError } from './chunk-5NOIGN5Y.mjs';
|
|
3
|
+
import { INTERNAL_TABLE_PREFIX } from './chunk-D7LAYTKN.mjs';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
|
|
6
|
+
var DumpOps = class {
|
|
7
|
+
constructor(conn, localNodeId, hlc, pkResolver) {
|
|
8
|
+
this.conn = conn;
|
|
9
|
+
this.localNodeId = localNodeId;
|
|
10
|
+
this.hlc = hlc;
|
|
11
|
+
this.pkResolver = pkResolver;
|
|
12
|
+
}
|
|
13
|
+
async *dumpTable(table, batchSize) {
|
|
14
|
+
if (!IDENTIFIER_RE.test(table)) {
|
|
15
|
+
throw new ReplicationError(`Invalid table name: ${table}`);
|
|
16
|
+
}
|
|
17
|
+
const total = await selectCountTableRows(this.conn, table);
|
|
18
|
+
if (total === 0) return;
|
|
19
|
+
const pkColumns = await this.pkResolver.forTable(table);
|
|
20
|
+
let offset = 0;
|
|
21
|
+
let batchNum = 0;
|
|
22
|
+
const needsRowid = pkColumns.length === 1 && pkColumns[0] === "rowid";
|
|
23
|
+
while (offset < total) {
|
|
24
|
+
const selectSql = needsRowid ? `SELECT rowid, * FROM "${table}" LIMIT ? OFFSET ?` : `SELECT * FROM "${table}" LIMIT ? OFFSET ?`;
|
|
25
|
+
const selectStmt = await this.conn.prepare(selectSql);
|
|
26
|
+
const rows = await selectStmt.all(batchSize, offset);
|
|
27
|
+
if (rows.length === 0) break;
|
|
28
|
+
const changes = [];
|
|
29
|
+
const hlcValue = this.hlc.now();
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
const primaryKey = {};
|
|
32
|
+
for (const col of pkColumns) {
|
|
33
|
+
if (col in row) {
|
|
34
|
+
primaryKey[col] = row[col];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const rowId = pkColumns.map((col) => String(row[col] ?? "")).join("-");
|
|
38
|
+
changes.push({
|
|
39
|
+
table,
|
|
40
|
+
operation: "insert",
|
|
41
|
+
rowId,
|
|
42
|
+
primaryKey,
|
|
43
|
+
hlc: hlcValue,
|
|
44
|
+
txId: `dump-${table}-${batchNum}`,
|
|
45
|
+
nodeId: this.localNodeId,
|
|
46
|
+
newData: row,
|
|
47
|
+
oldData: null
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const checksum = computeChecksum(changes);
|
|
51
|
+
const dumpEpoch = BigInt(Date.now()) * 1000000n;
|
|
52
|
+
const fromSeq = dumpEpoch + BigInt(offset);
|
|
53
|
+
const toSeq = dumpEpoch + BigInt(offset + rows.length - 1);
|
|
54
|
+
const dumpNodeId = `dump-${this.localNodeId}`;
|
|
55
|
+
yield {
|
|
56
|
+
sourceNodeId: dumpNodeId,
|
|
57
|
+
batchId: `dump-${table}-${batchNum}`,
|
|
58
|
+
fromSeq,
|
|
59
|
+
toSeq,
|
|
60
|
+
hlcRange: { min: hlcValue, max: hlcValue },
|
|
61
|
+
changes,
|
|
62
|
+
checksum
|
|
63
|
+
};
|
|
64
|
+
offset += rows.length;
|
|
65
|
+
batchNum += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
dumpTableOnConnection(conn, table, batchSize, startAfter) {
|
|
69
|
+
return dumpTablePages(conn, this.pkResolver, table, batchSize, startAfter);
|
|
70
|
+
}
|
|
71
|
+
async generateManifest(conn, table) {
|
|
72
|
+
if (!IDENTIFIER_RE.test(table)) {
|
|
73
|
+
throw new ReplicationError(`Invalid table name: ${table}`);
|
|
74
|
+
}
|
|
75
|
+
const rowCount = await selectCountTableRows(conn, table);
|
|
76
|
+
const hash = createHash("sha256");
|
|
77
|
+
const pkColumns = await this.pkResolver.forTableOnConnection(conn, table);
|
|
78
|
+
const needsRowid = pkColumns.length === 1 && pkColumns[0] === "rowid";
|
|
79
|
+
let lastPkValues = null;
|
|
80
|
+
const PK_BATCH_SIZE = 1e4;
|
|
81
|
+
let hasRows = true;
|
|
82
|
+
while (hasRows) {
|
|
83
|
+
let selectSql;
|
|
84
|
+
const params = [];
|
|
85
|
+
if (needsRowid) {
|
|
86
|
+
if (lastPkValues === null) {
|
|
87
|
+
selectSql = `SELECT rowid FROM "${table}" ORDER BY rowid LIMIT ?`;
|
|
88
|
+
} else {
|
|
89
|
+
selectSql = `SELECT rowid FROM "${table}" WHERE rowid > ? ORDER BY rowid LIMIT ?`;
|
|
90
|
+
params.push(lastPkValues[0]);
|
|
91
|
+
}
|
|
92
|
+
} else if (pkColumns.length === 1) {
|
|
93
|
+
const col = pkColumns[0];
|
|
94
|
+
if (lastPkValues === null) {
|
|
95
|
+
selectSql = `SELECT "${col}" FROM "${table}" ORDER BY "${col}" LIMIT ?`;
|
|
96
|
+
} else {
|
|
97
|
+
selectSql = `SELECT "${col}" FROM "${table}" WHERE "${col}" > ? ORDER BY "${col}" LIMIT ?`;
|
|
98
|
+
params.push(lastPkValues[0]);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const colList = pkColumns.map((c) => `"${c}"`).join(", ");
|
|
102
|
+
if (lastPkValues === null) {
|
|
103
|
+
selectSql = `SELECT ${colList} FROM "${table}" ORDER BY ${colList} LIMIT ?`;
|
|
104
|
+
} else {
|
|
105
|
+
const placeholders = pkColumns.map(() => "?").join(", ");
|
|
106
|
+
selectSql = `SELECT ${colList} FROM "${table}" WHERE (${colList}) > (${placeholders}) ORDER BY ${colList} LIMIT ?`;
|
|
107
|
+
params.push(...lastPkValues);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
params.push(PK_BATCH_SIZE);
|
|
111
|
+
const stmt = await conn.prepare(selectSql);
|
|
112
|
+
const rows = await stmt.all(...params);
|
|
113
|
+
if (rows.length === 0) {
|
|
114
|
+
hasRows = false;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
const pkVal = pkColumns.map((col) => String(row[col] ?? "")).join("-");
|
|
119
|
+
hash.update(pkVal);
|
|
120
|
+
}
|
|
121
|
+
if (rows.length < PK_BATCH_SIZE) {
|
|
122
|
+
hasRows = false;
|
|
123
|
+
} else {
|
|
124
|
+
const lastRow = rows[rows.length - 1];
|
|
125
|
+
lastPkValues = pkColumns.map((col) => lastRow[col]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { table, rowCount, pkHash: hash.digest("hex") };
|
|
129
|
+
}
|
|
130
|
+
async verifyManifest(manifest) {
|
|
131
|
+
const local = await this.generateManifest(this.conn, manifest.table);
|
|
132
|
+
return local.rowCount === manifest.rowCount && local.pkHash === manifest.pkHash;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
async function* dumpTablePages(conn, pkResolver, table, batchSize, startAfter) {
|
|
136
|
+
if (!IDENTIFIER_RE.test(table)) {
|
|
137
|
+
throw new ReplicationError(`Invalid table name: ${table}`);
|
|
138
|
+
}
|
|
139
|
+
const pkColumns = await pkResolver.forTableOnConnection(conn, table);
|
|
140
|
+
const needsRowid = pkColumns.length === 1 && pkColumns[0] === "rowid";
|
|
141
|
+
if (startAfter !== void 0 && startAfter.length !== pkColumns.length) {
|
|
142
|
+
throw new ReplicationError(
|
|
143
|
+
`Resume key for table '${table}' must carry ${pkColumns.length} value(s), received ${startAfter.length}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
let lastPkValues = startAfter ?? null;
|
|
147
|
+
let done = false;
|
|
148
|
+
while (!done) {
|
|
149
|
+
let selectSql;
|
|
150
|
+
const params = [];
|
|
151
|
+
if (lastPkValues === null) {
|
|
152
|
+
selectSql = needsRowid ? `SELECT rowid, * FROM "${table}" ORDER BY rowid LIMIT ?` : `SELECT * FROM "${table}" ORDER BY ${pkColumns.map((c) => `"${c}"`).join(", ")} LIMIT ?`;
|
|
153
|
+
params.push(batchSize);
|
|
154
|
+
} else if (needsRowid) {
|
|
155
|
+
selectSql = `SELECT rowid, * FROM "${table}" WHERE rowid > ? ORDER BY rowid LIMIT ?`;
|
|
156
|
+
params.push(lastPkValues[0], batchSize);
|
|
157
|
+
} else if (pkColumns.length === 1) {
|
|
158
|
+
selectSql = `SELECT * FROM "${table}" WHERE "${pkColumns[0]}" > ? ORDER BY "${pkColumns[0]}" LIMIT ?`;
|
|
159
|
+
params.push(lastPkValues[0], batchSize);
|
|
160
|
+
} else {
|
|
161
|
+
const colList = pkColumns.map((c) => `"${c}"`).join(", ");
|
|
162
|
+
const placeholders = pkColumns.map(() => "?").join(", ");
|
|
163
|
+
selectSql = `SELECT * FROM "${table}" WHERE (${colList}) > (${placeholders}) ORDER BY ${colList} LIMIT ?`;
|
|
164
|
+
params.push(...lastPkValues, batchSize);
|
|
165
|
+
}
|
|
166
|
+
const stmt = await conn.prepare(selectSql);
|
|
167
|
+
const rows = await stmt.all(...params);
|
|
168
|
+
if (rows.length === 0) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
const lastRow = rows[rows.length - 1];
|
|
172
|
+
lastPkValues = pkColumns.map((col) => lastRow[col]);
|
|
173
|
+
let isLast = rows.length < batchSize;
|
|
174
|
+
if (!isLast) {
|
|
175
|
+
let peekSql;
|
|
176
|
+
const peekParams = [];
|
|
177
|
+
if (needsRowid) {
|
|
178
|
+
peekSql = `SELECT 1 FROM "${table}" WHERE rowid > ? LIMIT 1`;
|
|
179
|
+
peekParams.push(lastPkValues[0]);
|
|
180
|
+
} else if (pkColumns.length === 1) {
|
|
181
|
+
peekSql = `SELECT 1 FROM "${table}" WHERE "${pkColumns[0]}" > ? LIMIT 1`;
|
|
182
|
+
peekParams.push(lastPkValues[0]);
|
|
183
|
+
} else {
|
|
184
|
+
const colList = pkColumns.map((c) => `"${c}"`).join(", ");
|
|
185
|
+
const placeholders = pkColumns.map(() => "?").join(", ");
|
|
186
|
+
peekSql = `SELECT 1 FROM "${table}" WHERE (${colList}) > (${placeholders}) LIMIT 1`;
|
|
187
|
+
peekParams.push(...lastPkValues);
|
|
188
|
+
}
|
|
189
|
+
const peekStmt = await conn.prepare(peekSql);
|
|
190
|
+
const peekRow = await peekStmt.get(...peekParams);
|
|
191
|
+
if (!peekRow) {
|
|
192
|
+
isLast = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
done = isLast;
|
|
196
|
+
const checksum = createHash("sha256").update(canonicaliseForChecksum(rows)).digest("hex");
|
|
197
|
+
yield { rows, checksum, isLast };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/core/sync/schema-dump.ts
|
|
202
|
+
async function dumpSchema(conn, excludePrefix = INTERNAL_TABLE_PREFIX) {
|
|
203
|
+
const rows = await selectUserSchemaObjects(conn, excludePrefix);
|
|
204
|
+
const filtered = rows.filter((row) => {
|
|
205
|
+
if (row.name.startsWith(excludePrefix)) return false;
|
|
206
|
+
if (row.name.startsWith("sqlite_")) return false;
|
|
207
|
+
if (row.tbl_name.startsWith(excludePrefix)) return false;
|
|
208
|
+
if (row.tbl_name.startsWith("sqlite_")) return false;
|
|
209
|
+
return validateDdlSafety(row.sql);
|
|
210
|
+
});
|
|
211
|
+
const tables = [];
|
|
212
|
+
const indexes = [];
|
|
213
|
+
for (const row of filtered) {
|
|
214
|
+
if (row.type === "table") {
|
|
215
|
+
tables.push({ name: row.name, sql: row.sql });
|
|
216
|
+
} else {
|
|
217
|
+
indexes.push({ sql: row.sql });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const tableOrder = await tablesInFkOrder(conn);
|
|
221
|
+
const orderMap = /* @__PURE__ */ new Map();
|
|
222
|
+
for (let i = 0; i < tableOrder.length; i++) {
|
|
223
|
+
orderMap.set(tableOrder[i], i);
|
|
224
|
+
}
|
|
225
|
+
tables.sort((a, b) => (orderMap.get(a.name) ?? 999) - (orderMap.get(b.name) ?? 999));
|
|
226
|
+
const result = [];
|
|
227
|
+
for (const t of tables) {
|
|
228
|
+
result.push(t.sql);
|
|
229
|
+
}
|
|
230
|
+
for (const idx of indexes) {
|
|
231
|
+
result.push(idx.sql);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
async function tablesInFkOrder(conn) {
|
|
236
|
+
const tableNames = await selectUserTableNames(conn, INTERNAL_TABLE_PREFIX);
|
|
237
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
238
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const name of tableNames) {
|
|
240
|
+
adjacency.set(name, /* @__PURE__ */ new Set());
|
|
241
|
+
inDegree.set(name, 0);
|
|
242
|
+
}
|
|
243
|
+
for (const name of tableNames) {
|
|
244
|
+
for (const referenced of await referencedTables(conn, name)) {
|
|
245
|
+
if (tableNames.includes(referenced) && referenced !== name) {
|
|
246
|
+
const deps = adjacency.get(referenced);
|
|
247
|
+
if (deps && !deps.has(name)) {
|
|
248
|
+
deps.add(name);
|
|
249
|
+
inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const queue = [];
|
|
255
|
+
for (const name of tableNames) {
|
|
256
|
+
if ((inDegree.get(name) ?? 0) === 0) {
|
|
257
|
+
queue.push(name);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const sorted = [];
|
|
261
|
+
while (queue.length > 0) {
|
|
262
|
+
const current = queue.shift();
|
|
263
|
+
if (current === void 0) break;
|
|
264
|
+
sorted.push(current);
|
|
265
|
+
const deps = adjacency.get(current);
|
|
266
|
+
if (deps) {
|
|
267
|
+
for (const dep of deps) {
|
|
268
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
269
|
+
inDegree.set(dep, newDeg);
|
|
270
|
+
if (newDeg === 0) {
|
|
271
|
+
queue.push(dep);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (sorted.length < tableNames.length) {
|
|
277
|
+
const remaining = tableNames.filter((n) => !sorted.includes(n)).sort();
|
|
278
|
+
sorted.push(...remaining);
|
|
279
|
+
}
|
|
280
|
+
return sorted;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export { DumpOps, dumpSchema, dumpTablePages, tablesInFkOrder };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { tokenizeSql, readSelectItems, findClauses } from './chunk-JU64Y7HM.mjs';
|
|
2
|
+
import { operationRegistryDigest } from './chunk-TUD5CJ76.mjs';
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import { resolve, dirname } from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
|
|
7
|
+
// src/codegen/manifest.ts
|
|
8
|
+
var OPERATION_MANIFEST_VERSION = 1;
|
|
9
|
+
function buildOperationManifest(registry) {
|
|
10
|
+
const databases = {};
|
|
11
|
+
for (const databaseId of Object.keys(registry).sort()) {
|
|
12
|
+
const operations = registry[databaseId] ?? {};
|
|
13
|
+
const reads = {};
|
|
14
|
+
const writes = {};
|
|
15
|
+
for (const name of Object.keys(operations.reads ?? {}).sort()) {
|
|
16
|
+
const read = operations.reads?.[name];
|
|
17
|
+
if (read !== void 0) reads[name] = readShape(read);
|
|
18
|
+
}
|
|
19
|
+
for (const name of Object.keys(operations.writes ?? {}).sort()) {
|
|
20
|
+
const write2 = operations.writes?.[name];
|
|
21
|
+
if (write2 !== void 0) writes[name] = writeShape(write2);
|
|
22
|
+
}
|
|
23
|
+
databases[databaseId] = { reads, writes };
|
|
24
|
+
}
|
|
25
|
+
return { version: OPERATION_MANIFEST_VERSION, digest: operationRegistryDigest(registry), databases };
|
|
26
|
+
}
|
|
27
|
+
function readShape(operation) {
|
|
28
|
+
const args = [...operation.args ?? []];
|
|
29
|
+
const identityArgs = Object.keys(operation.fromIdentity ?? {});
|
|
30
|
+
return { args, identityArgs, columns: statementColumns(operation, args, identityArgs) };
|
|
31
|
+
}
|
|
32
|
+
function writeShape(operation) {
|
|
33
|
+
return { args: [...operation.args ?? []], identityArgs: Object.keys(operation.fromIdentity ?? {}), columns: null };
|
|
34
|
+
}
|
|
35
|
+
function statementColumns(operation, args, identityArgs) {
|
|
36
|
+
if (operation.columns !== void 0) return [...operation.columns];
|
|
37
|
+
if (args.length > 0 || identityArgs.length > 0) return null;
|
|
38
|
+
try {
|
|
39
|
+
return selectColumns(operation.statement({}).sql);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function selectColumns(sql) {
|
|
45
|
+
const tokens = withoutTrailingSemicolon(tokenizeSql(sql));
|
|
46
|
+
if (tokens.length === 0 || tokens[0].lower !== "select" || tokens[0].quoted) return null;
|
|
47
|
+
const items = readSelectItems(sql, tokens, findClauses(sql, tokens));
|
|
48
|
+
const names = [];
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
if (item.star || item.alias === null) return null;
|
|
51
|
+
names.push(item.alias);
|
|
52
|
+
}
|
|
53
|
+
return names.length === 0 ? null : names;
|
|
54
|
+
}
|
|
55
|
+
function withoutTrailingSemicolon(tokens) {
|
|
56
|
+
const last = tokens[tokens.length - 1];
|
|
57
|
+
if (last !== void 0 && last.kind === "punct" && last.value === ";") return tokens.slice(0, -1);
|
|
58
|
+
return tokens;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/codegen/render.ts
|
|
62
|
+
var DEFAULT_PACKAGE = "@delali/sirannon-db";
|
|
63
|
+
function renderOperationTypes(manifest, options) {
|
|
64
|
+
const lines = [
|
|
65
|
+
`import type { OperationRef } from '${options?.packageName ?? DEFAULT_PACKAGE}'`,
|
|
66
|
+
"",
|
|
67
|
+
`export const registryDigest = ${JSON.stringify(manifest.digest ?? null)}`
|
|
68
|
+
];
|
|
69
|
+
const declared = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const databaseId of Object.keys(manifest.databases).sort()) {
|
|
71
|
+
const database = manifest.databases[databaseId];
|
|
72
|
+
lines.push("", ...renderDatabase(databaseId, database, declared));
|
|
73
|
+
}
|
|
74
|
+
return `${lines.join("\n")}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
function claim(declared, generated, source) {
|
|
78
|
+
const taken = declared.get(generated);
|
|
79
|
+
if (taken !== void 0 && taken !== source) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`'${source}' and '${taken}' both generate the identifier '${generated}'. Rename one of them so the generated file declares each name once.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
declared.set(generated, source);
|
|
85
|
+
return generated;
|
|
86
|
+
}
|
|
87
|
+
function renderDatabase(databaseId, database, declared) {
|
|
88
|
+
const prefix = pascalCase(databaseId);
|
|
89
|
+
const rowTypes = /* @__PURE__ */ new Map();
|
|
90
|
+
const lines = [];
|
|
91
|
+
for (const [name, shape] of Object.entries(database.reads)) {
|
|
92
|
+
if (shape.columns === null) continue;
|
|
93
|
+
const rowType = claim(declared, `${prefix}${pascalCase(name)}Row`, `${databaseId}.reads.${name}`);
|
|
94
|
+
rowTypes.set(name, rowType);
|
|
95
|
+
lines.push(`export interface ${rowType} {`);
|
|
96
|
+
for (const column of shape.columns) lines.push(` ${propertyKey(column)}: unknown`);
|
|
97
|
+
lines.push("}", "");
|
|
98
|
+
}
|
|
99
|
+
lines.push(`export const ${claim(declared, identifier(databaseId), databaseId)} = {`, " reads: {");
|
|
100
|
+
for (const [name, shape] of Object.entries(database.reads)) {
|
|
101
|
+
const row = rowTypes.get(name) ?? "Record<string, unknown>";
|
|
102
|
+
lines.push(
|
|
103
|
+
` ${propertyKey(name)}: { name: ${JSON.stringify(name)} } as OperationRef<${argsType(shape)}, ${row}>,`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
lines.push(" },", " writes: {");
|
|
107
|
+
for (const [name, shape] of Object.entries(database.writes)) {
|
|
108
|
+
lines.push(
|
|
109
|
+
` ${propertyKey(name)}: { name: ${JSON.stringify(name)} } as OperationRef<${argsType(shape)}, never>,`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
lines.push(" },", "}");
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
115
|
+
function argsType(shape) {
|
|
116
|
+
if (shape.args.length === 0) return "Record<string, never>";
|
|
117
|
+
return `{ ${shape.args.map((name) => `${propertyKey(name)}: unknown`).join("; ")} }`;
|
|
118
|
+
}
|
|
119
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
120
|
+
function propertyKey(name) {
|
|
121
|
+
return IDENTIFIER.test(name) ? name : JSON.stringify(name);
|
|
122
|
+
}
|
|
123
|
+
function identifier(value) {
|
|
124
|
+
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
125
|
+
return IDENTIFIER.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
126
|
+
}
|
|
127
|
+
function pascalCase(value) {
|
|
128
|
+
return value.split(/[^A-Za-z0-9]+/).filter((part) => part.length > 0).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
129
|
+
}
|
|
130
|
+
var CODEGEN_USAGE = `sirannon-codegen --registry <module> --out <file> [--manifest <file>] [--export <name>] [--package <name>]
|
|
131
|
+
|
|
132
|
+
Reads the operation registry a server is built from and writes the typed
|
|
133
|
+
references a client calls it through. The registry module is imported, so run
|
|
134
|
+
it under a loader that reads your source format when it is not JavaScript.`;
|
|
135
|
+
function parseOptions(argv) {
|
|
136
|
+
const values = /* @__PURE__ */ new Map();
|
|
137
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
138
|
+
const flag = argv[i];
|
|
139
|
+
const value = argv[i + 1];
|
|
140
|
+
if (!flag.startsWith("--") || value === void 0) {
|
|
141
|
+
throw new Error(`Unrecognised argument '${flag}'
|
|
142
|
+
|
|
143
|
+
${CODEGEN_USAGE}`);
|
|
144
|
+
}
|
|
145
|
+
values.set(flag.slice(2), value);
|
|
146
|
+
}
|
|
147
|
+
const registry = values.get("registry");
|
|
148
|
+
const out = values.get("out");
|
|
149
|
+
if (registry === void 0 || out === void 0) {
|
|
150
|
+
throw new Error(`--registry and --out are both required
|
|
151
|
+
|
|
152
|
+
${CODEGEN_USAGE}`);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
registry,
|
|
156
|
+
out,
|
|
157
|
+
manifest: values.get("manifest"),
|
|
158
|
+
exportName: values.get("export"),
|
|
159
|
+
packageName: values.get("package")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function readRegistry(module, exportName, path) {
|
|
163
|
+
const candidate = exportName === void 0 ? module.operations ?? module.default : module[exportName];
|
|
164
|
+
if (candidate === void 0 || typeof candidate !== "object") {
|
|
165
|
+
const named = exportName === void 0 ? "'operations' or a default export" : `'${exportName}'`;
|
|
166
|
+
throw new Error(`Module '${path}' exports no operation registry named ${named}`);
|
|
167
|
+
}
|
|
168
|
+
return candidate;
|
|
169
|
+
}
|
|
170
|
+
function write(path, contents) {
|
|
171
|
+
const target = resolve(path);
|
|
172
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
173
|
+
writeFileSync(target, contents);
|
|
174
|
+
}
|
|
175
|
+
async function runCodegen(argv) {
|
|
176
|
+
const options = parseOptions(argv);
|
|
177
|
+
const modulePath = resolve(options.registry);
|
|
178
|
+
const imported = await import(pathToFileURL(modulePath).href);
|
|
179
|
+
const manifest = buildOperationManifest(readRegistry(imported, options.exportName, modulePath));
|
|
180
|
+
write(options.out, renderOperationTypes(manifest, { packageName: options.packageName }));
|
|
181
|
+
if (options.manifest !== void 0) {
|
|
182
|
+
write(options.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { CODEGEN_USAGE, OPERATION_MANIFEST_VERSION, buildOperationManifest, renderOperationTypes, runCodegen, selectColumns };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { sha256Hex } from './chunk-GVCNMPOS.mjs';
|
|
2
|
+
|
|
3
|
+
// src/server/operation-lookup.ts
|
|
4
|
+
var DIGEST_FIELD_SEPARATOR = "\0";
|
|
5
|
+
function unknownOperation(name, databaseId) {
|
|
6
|
+
return {
|
|
7
|
+
status: 404,
|
|
8
|
+
code: "UNKNOWN_QUERY",
|
|
9
|
+
message: `No operation named '${name}' is registered for database '${databaseId}'`
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function ownEntry(group, key) {
|
|
13
|
+
if (group === void 0 || !Object.hasOwn(group, key)) return void 0;
|
|
14
|
+
return group[key];
|
|
15
|
+
}
|
|
16
|
+
function findRead(registry, databaseId, name) {
|
|
17
|
+
const operation = ownEntry(ownEntry(registry, databaseId)?.reads, name);
|
|
18
|
+
return operation ?? unknownOperation(name, databaseId);
|
|
19
|
+
}
|
|
20
|
+
function findWrite(registry, databaseId, name) {
|
|
21
|
+
const operation = ownEntry(ownEntry(registry, databaseId)?.writes, name);
|
|
22
|
+
return operation ?? unknownOperation(name, databaseId);
|
|
23
|
+
}
|
|
24
|
+
function isRefusal(value) {
|
|
25
|
+
return "status" in value && "code" in value && "message" in value;
|
|
26
|
+
}
|
|
27
|
+
function resolveArguments(operation, supplied, identity) {
|
|
28
|
+
const declared = operation.args ?? [];
|
|
29
|
+
const identityFields = operation.fromIdentity ?? {};
|
|
30
|
+
const resolved = {};
|
|
31
|
+
for (const name of Object.keys(supplied ?? {})) {
|
|
32
|
+
if (Object.hasOwn(identityFields, name)) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
refusal: {
|
|
36
|
+
status: 400,
|
|
37
|
+
code: "ARGUMENT_NOT_ALLOWED",
|
|
38
|
+
message: `Argument '${name}' is filled from the authenticated identity and cannot be supplied by the caller`
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!declared.includes(name)) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
refusal: {
|
|
46
|
+
status: 400,
|
|
47
|
+
code: "ARGUMENT_NOT_ALLOWED",
|
|
48
|
+
message: `Argument '${name}' is not declared by this operation`
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const name of declared) {
|
|
54
|
+
if (supplied === void 0 || !Object.hasOwn(supplied, name)) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
refusal: { status: 400, code: "MISSING_ARGUMENT", message: `Argument '${name}' is required` }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
resolved[name] = supplied[name];
|
|
61
|
+
}
|
|
62
|
+
for (const [name, field] of Object.entries(identityFields)) {
|
|
63
|
+
const source = identity;
|
|
64
|
+
const value = source === void 0 || source === null ? void 0 : source[field];
|
|
65
|
+
if (value === void 0) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
refusal: {
|
|
69
|
+
status: 401,
|
|
70
|
+
code: "IDENTITY_REQUIRED",
|
|
71
|
+
message: `Argument '${name}' is filled from identity field '${String(field)}', which this request does not carry`
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
resolved[name] = value;
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, value: resolved };
|
|
78
|
+
}
|
|
79
|
+
function createOperationSource(registry) {
|
|
80
|
+
return {
|
|
81
|
+
digest: operationRegistryDigest(registry),
|
|
82
|
+
resolve: (kind, databaseId, name, supplied, identity) => {
|
|
83
|
+
const operation = kind === "read" ? findRead(registry, databaseId, name) : findWrite(registry, databaseId, name);
|
|
84
|
+
if (isRefusal(operation)) return { ok: false, refusal: operation };
|
|
85
|
+
const args = resolveArguments(operation, supplied, identity);
|
|
86
|
+
if (!args.ok) return { ok: false, refusal: args.refusal };
|
|
87
|
+
if (kind === "read") {
|
|
88
|
+
return { ok: true, statements: [operation.statement(args.value)] };
|
|
89
|
+
}
|
|
90
|
+
const produced = operation.statements(args.value);
|
|
91
|
+
const statements = Array.isArray(produced) ? produced : [produced];
|
|
92
|
+
if (statements.length === 0) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
refusal: { status: 500, code: "INTERNAL_ERROR", message: `Operation '${name}' produced no statements` }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, statements };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function operationLine(databaseId, kind, name, operation) {
|
|
103
|
+
const declared = [...operation.args ?? []].sort();
|
|
104
|
+
const identityFields = Object.keys(
|
|
105
|
+
operation.fromIdentity ?? {}
|
|
106
|
+
).sort();
|
|
107
|
+
return [databaseId, kind, name, declared.join(","), identityFields.join(",")].join(DIGEST_FIELD_SEPARATOR);
|
|
108
|
+
}
|
|
109
|
+
function operationRegistryDigest(registry) {
|
|
110
|
+
if (registry === void 0) return void 0;
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (const databaseId of Object.keys(registry).sort()) {
|
|
113
|
+
const operations = registry[databaseId] ?? {};
|
|
114
|
+
for (const name of Object.keys(operations.reads ?? {}).sort()) {
|
|
115
|
+
const read = operations.reads?.[name];
|
|
116
|
+
if (read) lines.push(operationLine(databaseId, "read", name, read));
|
|
117
|
+
}
|
|
118
|
+
for (const name of Object.keys(operations.writes ?? {}).sort()) {
|
|
119
|
+
const write = operations.writes?.[name];
|
|
120
|
+
if (write) lines.push(operationLine(databaseId, "write", name, write));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return sha256Hex(lines.join("\n"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { createOperationSource, findRead, findWrite, isRefusal, operationRegistryDigest, resolveArguments };
|
|
@@ -53,6 +53,14 @@ var HookDeniedError = class extends SirannonError {
|
|
|
53
53
|
this.name = "HookDeniedError";
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
|
+
var RequestDeniedError = class extends SirannonError {
|
|
57
|
+
status;
|
|
58
|
+
constructor(status, code, message) {
|
|
59
|
+
super(message, code);
|
|
60
|
+
this.name = "RequestDeniedError";
|
|
61
|
+
this.status = status;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
56
64
|
var CDCError = class extends SirannonError {
|
|
57
65
|
constructor(message) {
|
|
58
66
|
super(message, "CDC_ERROR");
|
|
@@ -101,4 +109,4 @@ var ExtensionError = class extends SirannonError {
|
|
|
101
109
|
}
|
|
102
110
|
};
|
|
103
111
|
|
|
104
|
-
export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError, WriteOverloadError };
|
|
112
|
+
export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, RequestDeniedError, SirannonError, TransactionError, WriteOverloadError };
|