@bitcobblers/wod-wiki-storage 0.39.2143 → 0.40.2144
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/chunk-IFLOX2TK.mjs +874 -0
- package/dist/chunk-IFLOX2TK.mjs.map +1 -0
- package/dist/{chunk-NUJEAUAK.mjs → chunk-IMNG2HY5.mjs} +4 -131
- package/dist/chunk-IMNG2HY5.mjs.map +1 -0
- package/dist/chunk-JFVXIJJI.mjs +134 -0
- package/dist/chunk-JFVXIJJI.mjs.map +1 -0
- package/dist/chunk-MOGQICXM.mjs +43 -0
- package/dist/chunk-MOGQICXM.mjs.map +1 -0
- package/dist/idb/index.cjs +1462 -0
- package/dist/idb/index.cjs.map +1 -0
- package/dist/idb/index.d.cts +156 -0
- package/dist/idb/index.d.ts +156 -0
- package/dist/idb/index.mjs +1079 -0
- package/dist/idb/index.mjs.map +1 -0
- package/dist/index.cjs +47 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +5 -2
- package/dist/index.mjs.map +1 -1
- package/dist/policy-CPscN81J.d.ts +49 -0
- package/dist/policy-DRYF6Y8i.d.cts +49 -0
- package/dist/sql/index.cjs +241 -38
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +64 -132
- package/dist/sql/index.d.ts +64 -132
- package/dist/sql/index.mjs +188 -852
- package/dist/sql/index.mjs.map +1 -1
- package/dist/sync-Bjj_c1ea.d.ts +141 -0
- package/dist/sync-DG4Z-szj.d.cts +141 -0
- package/dist/{wire-CKJDK_Nq.d.cts → wire-DGYxXt3I.d.cts} +2 -2
- package/dist/{wire-CKJDK_Nq.d.ts → wire-DGYxXt3I.d.ts} +2 -2
- package/package.json +14 -7
- package/dist/chunk-NUJEAUAK.mjs.map +0 -1
package/dist/sql/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DOMAIN_PROJECTION_VERSION, bufferToOps, bufferedGet, overlayRows, isDirty, bufferSlot } from '../chunk-JFVXIJJI.mjs';
|
|
2
|
+
import { getRows, storeDefinition, deleteRows, clearRows, SCHEMA_TABLE, decodeRowJson, initializeStore, putRow, wipeRows, getRow, listRowsFromIndexMany, listRows, countRows, putRows, deleteIn, batchInsert, SYNC_STATE_TABLE, encodeRowJson } from '../chunk-IFLOX2TK.mjs';
|
|
3
|
+
export { SCHEMA_TABLE, SYNC_OPS_TABLE, SYNC_STATE_TABLE, applySyncBatch, clearRows, countRows, decodeRowJson, deleteRows, encodeRowJson, fetchSyncOps, getRow, getRows, initSyncTarget, initializeStore, listRows, listRowsFromIndexMany, probeSyncTarget, putRow, putRows, readLegacyRows, readRawSyncOps, storeDefinition, wipeRows } from '../chunk-IFLOX2TK.mjs';
|
|
4
|
+
import { rowsEqual, validateStoredPins, DB_VERSION, STORE_DEFS, stampUserOwned, rangeFromIDB, keyFromValue, isRange } from '../chunk-IMNG2HY5.mjs';
|
|
2
5
|
import { libsqlDatabase } from '../chunk-6ZTAWTAP.mjs';
|
|
3
6
|
export { libsqlDatabase } from '../chunk-6ZTAWTAP.mjs';
|
|
4
7
|
import { StorageError } from '../chunk-XRQKLSOT.mjs';
|
|
@@ -6,606 +9,6 @@ export { StorageError } from '../chunk-XRQKLSOT.mjs';
|
|
|
6
9
|
import { catalogOfItem, sourceMatches } from '@bitcobblers/wod-wiki-wql';
|
|
7
10
|
import { createClient } from '@libsql/client/web';
|
|
8
11
|
|
|
9
|
-
// src/sql/codec.ts
|
|
10
|
-
var TAG = "$binary";
|
|
11
|
-
var LEN = "$bytes";
|
|
12
|
-
function bytesToBase64(bytes) {
|
|
13
|
-
let binary = "";
|
|
14
|
-
const CHUNK = 32768;
|
|
15
|
-
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
16
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
17
|
-
}
|
|
18
|
-
return btoa(binary);
|
|
19
|
-
}
|
|
20
|
-
function base64ToBytes(base64) {
|
|
21
|
-
const binary = atob(base64);
|
|
22
|
-
const bytes = new Uint8Array(binary.length);
|
|
23
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
24
|
-
return bytes;
|
|
25
|
-
}
|
|
26
|
-
function reviveBinary(value) {
|
|
27
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
28
|
-
const keys = Object.keys(value);
|
|
29
|
-
if (keys.length !== 2 || !keys.includes(TAG) || !keys.includes(LEN)) return value;
|
|
30
|
-
const tag = value;
|
|
31
|
-
if (typeof tag[TAG] !== "string" || typeof tag[LEN] !== "number") return value;
|
|
32
|
-
const bytes = base64ToBytes(tag[TAG]);
|
|
33
|
-
if (bytes.length !== tag[LEN]) {
|
|
34
|
-
throw new Error(`corrupt binary cell: declared ${tag[LEN]} bytes, decoded ${bytes.length}`);
|
|
35
|
-
}
|
|
36
|
-
return bytes.buffer;
|
|
37
|
-
}
|
|
38
|
-
var canonicalJson = (_key, value) => {
|
|
39
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
40
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, Reflect.get(value, key)]));
|
|
41
|
-
};
|
|
42
|
-
function encodeRowJson(value, binaryField) {
|
|
43
|
-
if (binaryField && value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
44
|
-
const row = value;
|
|
45
|
-
const data = row[binaryField];
|
|
46
|
-
if (data instanceof ArrayBuffer) {
|
|
47
|
-
const bytes = new Uint8Array(data);
|
|
48
|
-
return JSON.stringify({ ...row, [binaryField]: { [TAG]: bytesToBase64(bytes), [LEN]: bytes.length } }, canonicalJson);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return JSON.stringify(value, canonicalJson);
|
|
52
|
-
}
|
|
53
|
-
function decodeRowJson(text2, binaryField) {
|
|
54
|
-
const row = JSON.parse(text2);
|
|
55
|
-
if (binaryField && row !== null && typeof row === "object" && !Array.isArray(row)) {
|
|
56
|
-
const record = row;
|
|
57
|
-
const revived = reviveBinary(record[binaryField]);
|
|
58
|
-
if (revived !== record[binaryField]) record[binaryField] = revived;
|
|
59
|
-
}
|
|
60
|
-
return row;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/sql/store.ts
|
|
64
|
-
var SCHEMA_TABLE = "_wodwiki_schema";
|
|
65
|
-
var SCHEMA_KEY = "schema";
|
|
66
|
-
var ADDED_STORES_BY_VERSION = {
|
|
67
|
-
31: ["equipment", "block_equipment"]
|
|
68
|
-
};
|
|
69
|
-
function additiveStoresSince(stored) {
|
|
70
|
-
const added = /* @__PURE__ */ new Set();
|
|
71
|
-
for (const [version, names] of Object.entries(ADDED_STORES_BY_VERSION)) {
|
|
72
|
-
if (Number(version) > stored) for (const name of names) added.add(name);
|
|
73
|
-
}
|
|
74
|
-
return added;
|
|
75
|
-
}
|
|
76
|
-
var BINARY_FIELD = { attachments: "data" };
|
|
77
|
-
var binaryFieldOf = (def) => BINARY_FIELD[def.name];
|
|
78
|
-
function storeDefinition(name) {
|
|
79
|
-
const def = STORE_DEFS.find((d) => d.name === name);
|
|
80
|
-
if (!def) throw new StorageError(404, `unknown store: ${name}`);
|
|
81
|
-
return def;
|
|
82
|
-
}
|
|
83
|
-
function indexDefinition(def, indexName) {
|
|
84
|
-
const idx = def.indexes.find((i) => i.name === indexName);
|
|
85
|
-
if (!idx) throw new StorageError(404, `unknown index: ${def.name}.${indexName}`);
|
|
86
|
-
return idx;
|
|
87
|
-
}
|
|
88
|
-
function sanitize(name) {
|
|
89
|
-
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
90
|
-
}
|
|
91
|
-
var indexCol = (indexName, part) => `i_${sanitize(indexName)}_${part}`;
|
|
92
|
-
function keyCols(def) {
|
|
93
|
-
return def.keyPath.map((_, i) => `k${i}`);
|
|
94
|
-
}
|
|
95
|
-
var junctionTable = (def, idx) => `${def.name}__${sanitize(idx.name)}`;
|
|
96
|
-
var multiEntryIndex = (def) => def.indexes.find((i) => i.multiEntry);
|
|
97
|
-
function pathType(def, path) {
|
|
98
|
-
return COLUMN_TYPES[def.name][path] ?? "text";
|
|
99
|
-
}
|
|
100
|
-
function colSql(dialect, type) {
|
|
101
|
-
if (dialect === "sqlite") return "";
|
|
102
|
-
return type === "text" ? ' TEXT COLLATE "C"' : " DOUBLE PRECISION";
|
|
103
|
-
}
|
|
104
|
-
function toSqlScalar(v) {
|
|
105
|
-
if (v === null || v === void 0) return null;
|
|
106
|
-
if (typeof v === "boolean") return v ? 1 : 0;
|
|
107
|
-
if (typeof v === "number" || typeof v === "string") return v;
|
|
108
|
-
return JSON.stringify(v);
|
|
109
|
-
}
|
|
110
|
-
function validateKey(key) {
|
|
111
|
-
const scalar = (v) => typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
112
|
-
if (!scalar(key) && !(Array.isArray(key) && key.length > 0 && key.every(scalar))) {
|
|
113
|
-
throw new StorageError(400, "key must be a string, finite number, or non-empty scalar tuple");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function validateRange(range) {
|
|
117
|
-
if (range === void 0) return;
|
|
118
|
-
if (range === null || typeof range !== "object" || Array.isArray(range)) {
|
|
119
|
-
throw new StorageError(400, "range must be an object");
|
|
120
|
-
}
|
|
121
|
-
for (const key of [range.only, range.lower, range.upper]) {
|
|
122
|
-
if (key !== void 0) validateKey(key);
|
|
123
|
-
}
|
|
124
|
-
for (const open of [range.lowerOpen, range.upperOpen]) {
|
|
125
|
-
if (open !== void 0 && typeof open !== "boolean") {
|
|
126
|
-
throw new StorageError(400, "range open flags must be boolean");
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function toParts(key) {
|
|
131
|
-
validateKey(key);
|
|
132
|
-
return Array.isArray(key) ? key : [key];
|
|
133
|
-
}
|
|
134
|
-
function arrayPart(value, dotted) {
|
|
135
|
-
let cur = value;
|
|
136
|
-
for (const part of dotted.split(".")) {
|
|
137
|
-
if (cur === null || typeof cur !== "object") return void 0;
|
|
138
|
-
const row = cur;
|
|
139
|
-
cur = row[part];
|
|
140
|
-
}
|
|
141
|
-
return Array.isArray(cur) ? cur : void 0;
|
|
142
|
-
}
|
|
143
|
-
function requireKey(def, value) {
|
|
144
|
-
const parts = def.keyPath.map((p) => extractKeyPart(value, p));
|
|
145
|
-
for (const [i, part] of parts.entries()) {
|
|
146
|
-
if (part === void 0) {
|
|
147
|
-
throw new StorageError(400, `keyPath part '${def.keyPath[i]}' missing on ${def.name} value`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const key = parts;
|
|
151
|
-
return key.length === 1 ? key[0] : key;
|
|
152
|
-
}
|
|
153
|
-
function rangeWhere(cols, range, params) {
|
|
154
|
-
if (!range) return "";
|
|
155
|
-
if (range.only !== void 0) {
|
|
156
|
-
const parts = toParts(range.only);
|
|
157
|
-
if (parts.length > cols.length) return null;
|
|
158
|
-
const clauses2 = parts.map((p, i) => {
|
|
159
|
-
params.push(toSqlScalar(p));
|
|
160
|
-
return `"${cols[i]}" = ?`;
|
|
161
|
-
});
|
|
162
|
-
return ` WHERE ${clauses2.join(" AND ")}`;
|
|
163
|
-
}
|
|
164
|
-
const bounds = [];
|
|
165
|
-
if (range.lower !== void 0 && range.lower !== null) {
|
|
166
|
-
bounds.push([toParts(range.lower), range.lowerOpen ? ">" : ">="]);
|
|
167
|
-
}
|
|
168
|
-
if (range.upper !== void 0 && range.upper !== null) {
|
|
169
|
-
bounds.push([toParts(range.upper), range.upperOpen ? "<" : "<="]);
|
|
170
|
-
}
|
|
171
|
-
const clauses = [];
|
|
172
|
-
for (const [parts, op] of bounds) {
|
|
173
|
-
if (parts.length > cols.length) return null;
|
|
174
|
-
if (parts.length === 1) {
|
|
175
|
-
params.push(toSqlScalar(parts[0]));
|
|
176
|
-
clauses.push(`"${cols[0]}" ${op} ?`);
|
|
177
|
-
} else {
|
|
178
|
-
const ors = parts.map((p, i) => {
|
|
179
|
-
const eqs = [];
|
|
180
|
-
for (let j = 0; j < i; j++) {
|
|
181
|
-
params.push(toSqlScalar(parts[j]));
|
|
182
|
-
eqs.push(`"${cols[j]}" = ?`);
|
|
183
|
-
}
|
|
184
|
-
params.push(toSqlScalar(p));
|
|
185
|
-
const cmpOp = i === parts.length - 1 ? op : op[0];
|
|
186
|
-
eqs.push(`"${cols[i]}" ${cmpOp} ?`);
|
|
187
|
-
return `(${eqs.join(" AND ")})`;
|
|
188
|
-
});
|
|
189
|
-
clauses.push(`(${ors.join(" OR ")})`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
193
|
-
}
|
|
194
|
-
function clampLimit(count) {
|
|
195
|
-
if (count === void 0) return void 0;
|
|
196
|
-
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
197
|
-
throw new StorageError(400, `invalid count: ${String(count)}`);
|
|
198
|
-
}
|
|
199
|
-
return Math.max(0, Math.floor(count));
|
|
200
|
-
}
|
|
201
|
-
var limitClause = (count) => count === void 0 ? "" : ` LIMIT ${clampLimit(count)}`;
|
|
202
|
-
function parseRows(rows, binaryField) {
|
|
203
|
-
return rows.map((r) => decodeRowJson(r.data, binaryField));
|
|
204
|
-
}
|
|
205
|
-
async function scanRows(conn, def, idx, range, limit) {
|
|
206
|
-
const rows = await conn.query(`SELECT "data" FROM "${def.name}"`);
|
|
207
|
-
const keyed = [];
|
|
208
|
-
for (const row of rows) {
|
|
209
|
-
const value = decodeRowJson(row.data, binaryFieldOf(def));
|
|
210
|
-
if (idx) {
|
|
211
|
-
const parts = idx.keyPath.map((p) => extractKeyPart(value, p));
|
|
212
|
-
if (!parts.every((p) => p !== void 0)) continue;
|
|
213
|
-
const key = parts.length === 1 ? parts[0] : parts;
|
|
214
|
-
if (range && !keyInRange(key, range)) continue;
|
|
215
|
-
keyed.push([key, value]);
|
|
216
|
-
} else {
|
|
217
|
-
const key = requireKey(def, value);
|
|
218
|
-
if (range && !keyInRange(key, range)) continue;
|
|
219
|
-
keyed.push([key, value]);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
keyed.sort((a, b) => compareKeys(a[0], b[0]));
|
|
223
|
-
const out = keyed.map(([, v]) => v);
|
|
224
|
-
return limit === void 0 ? out : out.slice(0, limit);
|
|
225
|
-
}
|
|
226
|
-
async function getRow(conn, def, key) {
|
|
227
|
-
const parts = toParts(key);
|
|
228
|
-
if (parts.length !== def.keyPath.length) {
|
|
229
|
-
throw new StorageError(400, `key arity ${parts.length} != keyPath arity ${def.keyPath.length} for ${def.name}`);
|
|
230
|
-
}
|
|
231
|
-
const cond = keyCols(def).map((c) => `"${c}" = ?`).join(" AND ");
|
|
232
|
-
const rows = await conn.query(
|
|
233
|
-
`SELECT "data" FROM "${def.name}" WHERE ${cond}`,
|
|
234
|
-
parts.map(toSqlScalar)
|
|
235
|
-
);
|
|
236
|
-
return rows[0] ? decodeRowJson(rows[0].data, binaryFieldOf(def)) : void 0;
|
|
237
|
-
}
|
|
238
|
-
async function getRows(conn, def, keys) {
|
|
239
|
-
const found = /* @__PURE__ */ new Map();
|
|
240
|
-
for (const part of chunks(keys, 50)) {
|
|
241
|
-
const params = [];
|
|
242
|
-
const conditions = part.map((key) => {
|
|
243
|
-
const parts = toParts(key);
|
|
244
|
-
if (parts.length !== def.keyPath.length) throw new StorageError(400, "invalid compound key arity");
|
|
245
|
-
params.push(...parts.map(toSqlScalar));
|
|
246
|
-
return `(${keyCols(def).map((col) => `"${col}" = ?`).join(" AND ")})`;
|
|
247
|
-
});
|
|
248
|
-
for (const row of await conn.query(`SELECT "data" FROM "${def.name}" WHERE ${conditions.join(" OR ")}`, params)) {
|
|
249
|
-
const value = decodeRowJson(String(row.data), binaryFieldOf(def));
|
|
250
|
-
found.set(JSON.stringify(requireKey(def, value)), value);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return keys.map((key) => found.get(JSON.stringify(key)));
|
|
254
|
-
}
|
|
255
|
-
async function listRowsFromIndexMany(conn, def, indexName, keys) {
|
|
256
|
-
const idx = indexDefinition(def, indexName);
|
|
257
|
-
if (idx.multiEntry) {
|
|
258
|
-
const out = [];
|
|
259
|
-
for (const key of keys) out.push(await listRows(conn, def, { only: key }, void 0, indexName));
|
|
260
|
-
return out;
|
|
261
|
-
}
|
|
262
|
-
const found = /* @__PURE__ */ new Map();
|
|
263
|
-
for (const part of chunks(keys, 50)) {
|
|
264
|
-
const params = [];
|
|
265
|
-
const conditions = part.map((key) => {
|
|
266
|
-
const parts = toParts(key);
|
|
267
|
-
if (parts.length !== idx.keyPath.length) throw new StorageError(400, "invalid index key arity");
|
|
268
|
-
params.push(...parts.map(toSqlScalar));
|
|
269
|
-
return `(${parts.map((_, i) => `"${indexCol(idx.name, i)}" = ?`).join(" AND ")})`;
|
|
270
|
-
});
|
|
271
|
-
for (const row of await conn.query(`SELECT "data" FROM "${def.name}" WHERE ${conditions.join(" OR ")} ORDER BY ${keyCols(def).map((col) => `"${col}"`).join(", ")}`, params)) {
|
|
272
|
-
const value = decodeRowJson(String(row.data), binaryFieldOf(def));
|
|
273
|
-
const parts = idx.keyPath.map((path) => extractKeyPart(value, path));
|
|
274
|
-
const key = JSON.stringify(parts.length === 1 ? parts[0] : parts);
|
|
275
|
-
const bucket = found.get(key);
|
|
276
|
-
if (bucket) bucket.push(value);
|
|
277
|
-
else found.set(key, [value]);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return keys.map((key) => found.get(JSON.stringify(key)) ?? []);
|
|
281
|
-
}
|
|
282
|
-
async function listRows(conn, def, range, count, indexName) {
|
|
283
|
-
validateRange(range);
|
|
284
|
-
const idx = indexName ? indexDefinition(def, indexName) : void 0;
|
|
285
|
-
const binaryField = binaryFieldOf(def);
|
|
286
|
-
if (idx?.multiEntry) {
|
|
287
|
-
const only = range?.only;
|
|
288
|
-
if (only === void 0 || typeof only === "object") {
|
|
289
|
-
throw new StorageError(400, `multi-entry index ${def.name}.${idx.name} supports exact-match queries only`);
|
|
290
|
-
}
|
|
291
|
-
const joinOn = keyCols(def).map((c) => `j."${c}" = e."${c}"`).join(" AND ");
|
|
292
|
-
const order2 = ['j."v"', ...keyCols(def).map((c) => `j."${c}"`)].join(", ");
|
|
293
|
-
const rows2 = await conn.query(
|
|
294
|
-
`SELECT e."data" AS "data" FROM "${def.name}" e JOIN "${junctionTable(def, idx)}" j ON ${joinOn} WHERE j."v" = ? ORDER BY ${order2}${limitClause(count)}`,
|
|
295
|
-
[JSON.stringify(only)]
|
|
296
|
-
);
|
|
297
|
-
return parseRows(rows2, binaryField);
|
|
298
|
-
}
|
|
299
|
-
const cols = idx ? idx.keyPath.map((_, part) => indexCol(idx.name, part)) : keyCols(def);
|
|
300
|
-
const params = [];
|
|
301
|
-
const where = rangeWhere(cols, range, params);
|
|
302
|
-
if (where === null) return scanRows(conn, def, idx, range, clampLimit(count));
|
|
303
|
-
const notNull = cols.map((c) => `"${c}" IS NOT NULL`).join(" AND ");
|
|
304
|
-
const cond = where ? `${where} AND ${notNull}` : ` WHERE ${notNull}`;
|
|
305
|
-
const keyNames = keyCols(def).filter((k) => !cols.includes(k));
|
|
306
|
-
const order = ` ORDER BY ${[...cols, ...keyNames].map((c) => `"${c}"`).join(", ")}`;
|
|
307
|
-
const rows = await conn.query(
|
|
308
|
-
`SELECT "data" FROM "${def.name}"${cond}${order}${limitClause(count)}`,
|
|
309
|
-
params
|
|
310
|
-
);
|
|
311
|
-
return parseRows(rows, binaryField);
|
|
312
|
-
}
|
|
313
|
-
async function countRows(conn, def, range, indexName) {
|
|
314
|
-
validateRange(range);
|
|
315
|
-
const idx = indexName ? indexDefinition(def, indexName) : void 0;
|
|
316
|
-
if (idx?.multiEntry) {
|
|
317
|
-
const only = range?.only;
|
|
318
|
-
if (only === void 0 || typeof only === "object") {
|
|
319
|
-
throw new StorageError(400, `multi-entry index ${def.name}.${idx.name} supports exact-match queries only`);
|
|
320
|
-
}
|
|
321
|
-
const joinOn = keyCols(def).map((c) => `j."${c}" = e."${c}"`).join(" AND ");
|
|
322
|
-
const rows2 = await conn.query(
|
|
323
|
-
`SELECT COUNT(*) AS "n" FROM "${def.name}" e JOIN "${junctionTable(def, idx)}" j ON ${joinOn} WHERE j."v" = ?`,
|
|
324
|
-
[JSON.stringify(only)]
|
|
325
|
-
);
|
|
326
|
-
return Number(rows2[0]?.n ?? 0);
|
|
327
|
-
}
|
|
328
|
-
const cols = idx ? idx.keyPath.map((_, part) => indexCol(idx.name, part)) : keyCols(def);
|
|
329
|
-
const params = [];
|
|
330
|
-
const where = rangeWhere(cols, range, params);
|
|
331
|
-
if (where === null) {
|
|
332
|
-
const all = await scanRows(conn, def, idx, range, void 0);
|
|
333
|
-
return all.length;
|
|
334
|
-
}
|
|
335
|
-
const cond = idx ? where ? `${where} AND ${cols.map((c) => `"${c}" IS NOT NULL`).join(" AND ")}` : ` WHERE ${cols.map((c) => `"${c}" IS NOT NULL`).join(" AND ")}` : where;
|
|
336
|
-
const rows = await conn.query(`SELECT COUNT(*) AS "n" FROM "${def.name}"${cond}`, params);
|
|
337
|
-
return Number(rows[0]?.n ?? 0);
|
|
338
|
-
}
|
|
339
|
-
var PARAM_BUDGET = 2e4;
|
|
340
|
-
var ROW_CAP = 100;
|
|
341
|
-
var chunks = (arr, size) => {
|
|
342
|
-
const out = [];
|
|
343
|
-
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
344
|
-
return out;
|
|
345
|
-
};
|
|
346
|
-
async function batchInsert(conn, table, cols, rows, onConflict) {
|
|
347
|
-
if (rows.length === 0) return;
|
|
348
|
-
const collist = cols.map((c) => `"${c}"`).join(", ");
|
|
349
|
-
const tuple = `(${cols.map(() => "?").join(", ")})`;
|
|
350
|
-
const per = Math.max(1, Math.min(ROW_CAP, Math.floor(PARAM_BUDGET / cols.length)));
|
|
351
|
-
const statements = chunks(rows, per).map((part) => ({
|
|
352
|
-
sql: `INSERT INTO "${table}" (${collist}) VALUES ${part.map(() => tuple).join(", ")} ${onConflict}`,
|
|
353
|
-
params: part.flat()
|
|
354
|
-
}));
|
|
355
|
-
if (conn.batch) await conn.batch(statements);
|
|
356
|
-
else for (const statement of statements) await conn.exec(statement.sql, statement.params);
|
|
357
|
-
}
|
|
358
|
-
async function deleteIn(conn, table, col, vals) {
|
|
359
|
-
for (const part of chunks(vals, 500)) {
|
|
360
|
-
if (part.length === 0) continue;
|
|
361
|
-
await conn.exec(`DELETE FROM "${table}" WHERE "${col}" IN (${part.map(() => "?").join(", ")})`, part);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
async function putRows(conn, def, values, changedKeys) {
|
|
365
|
-
if (values.length === 0) return [];
|
|
366
|
-
for (const value of values) validateStorageRow(def.name, value);
|
|
367
|
-
const keys = keyCols(def);
|
|
368
|
-
const cols = [...keys];
|
|
369
|
-
for (const idx of def.indexes) {
|
|
370
|
-
if (idx.multiEntry) continue;
|
|
371
|
-
idx.keyPath.forEach((_, part) => cols.push(indexCol(idx.name, part)));
|
|
372
|
-
}
|
|
373
|
-
cols.push("data");
|
|
374
|
-
const keySet = new Set(keys);
|
|
375
|
-
const updates = cols.filter((c) => !keySet.has(c)).map((c) => `"${c}" = excluded."${c}"`).join(", ");
|
|
376
|
-
const pk = keys.map((k) => `"${k}"`).join(", ");
|
|
377
|
-
const rows = [];
|
|
378
|
-
const out = [];
|
|
379
|
-
const junction = [];
|
|
380
|
-
const multi = multiEntryIndex(def);
|
|
381
|
-
for (const value of values) {
|
|
382
|
-
const k = requireKey(def, value);
|
|
383
|
-
const keyParts = toParts(k).map(toSqlScalar);
|
|
384
|
-
const vals = [...keyParts];
|
|
385
|
-
for (const idx of def.indexes) {
|
|
386
|
-
if (idx.multiEntry) continue;
|
|
387
|
-
idx.keyPath.forEach((path) => vals.push(toSqlScalar(extractKeyPart(value, path) ?? null)));
|
|
388
|
-
}
|
|
389
|
-
vals.push(encodeRowJson(value, binaryFieldOf(def)));
|
|
390
|
-
rows.push(vals);
|
|
391
|
-
out.push(k);
|
|
392
|
-
if (multi) {
|
|
393
|
-
const items = arrayPart(value, multi.keyPath[0]);
|
|
394
|
-
if (Array.isArray(items)) {
|
|
395
|
-
for (const item of items) junction.push([...keyParts, JSON.stringify(item)]);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
const tuple = `(${cols.map(() => "?").join(", ")})`;
|
|
400
|
-
const per = Math.max(1, Math.min(ROW_CAP, Math.floor(PARAM_BUDGET / cols.length)));
|
|
401
|
-
const statements = chunks(rows, per).map((part) => ({
|
|
402
|
-
sql: `INSERT INTO "${def.name}" (${cols.map((col) => `"${col}"`).join(", ")}) VALUES ${part.map(() => tuple).join(", ")} ` + (def.name === "block_revisions" ? `ON CONFLICT (${pk}) DO NOTHING` : `ON CONFLICT (${pk}) DO UPDATE SET ${updates} WHERE "${def.name}"."data" <> excluded."data"`) + ` RETURNING ${keys.map((col) => `"${col}"`).join(", ")}`,
|
|
403
|
-
params: part.flat()
|
|
404
|
-
}));
|
|
405
|
-
let returned;
|
|
406
|
-
try {
|
|
407
|
-
if (conn.batch) returned = await conn.batch(statements);
|
|
408
|
-
else {
|
|
409
|
-
returned = [];
|
|
410
|
-
for (const statement of statements) returned.push(await conn.query(statement.sql, statement.params));
|
|
411
|
-
}
|
|
412
|
-
} catch (error) {
|
|
413
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
-
for (const code of ["revision_conflict", "transaction_conflict", "invalid_session_pin"]) {
|
|
415
|
-
if (message.includes(code)) throw new StorageError(409, message, code);
|
|
416
|
-
}
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
419
|
-
const changed = returned.flat().map((row) => {
|
|
420
|
-
const parts = keys.map((col) => row[col]);
|
|
421
|
-
return parts.length === 1 ? parts[0] : parts;
|
|
422
|
-
});
|
|
423
|
-
changedKeys?.push(...changed);
|
|
424
|
-
if (multi) {
|
|
425
|
-
const jt = junctionTable(def, multi);
|
|
426
|
-
const changedSet = new Set(changed.map((key) => JSON.stringify(key)));
|
|
427
|
-
const deletes = values.flatMap((value) => {
|
|
428
|
-
const key = requireKey(def, value);
|
|
429
|
-
if (!changedSet.has(JSON.stringify(key))) return [];
|
|
430
|
-
const items = arrayPart(value, multi.keyPath[0]) ?? [];
|
|
431
|
-
return [{
|
|
432
|
-
sql: `DELETE FROM "${jt}" WHERE ${keys.map((col) => `"${col}" = ?`).join(" AND ")}` + (items.length ? ` AND "v" NOT IN (${items.map(() => "?").join(", ")})` : ""),
|
|
433
|
-
params: [...toParts(key).map(toSqlScalar), ...items.map((item) => JSON.stringify(item))]
|
|
434
|
-
}];
|
|
435
|
-
});
|
|
436
|
-
if (conn.batch && deletes.length) await conn.batch(deletes);
|
|
437
|
-
else for (const statement of deletes) await conn.exec(statement.sql, statement.params);
|
|
438
|
-
await batchInsert(conn, jt, [...keys, "v"], junction.filter((row) => changedSet.has(JSON.stringify(keys.length === 1 ? row[0] : row.slice(0, keys.length)))), "ON CONFLICT DO NOTHING");
|
|
439
|
-
}
|
|
440
|
-
return out;
|
|
441
|
-
}
|
|
442
|
-
async function putRow(conn, def, value) {
|
|
443
|
-
return (await putRows(conn, def, [value]))[0];
|
|
444
|
-
}
|
|
445
|
-
async function deleteRows(conn, def, keyOrRange) {
|
|
446
|
-
if (def.name === "block_revisions") throw new StorageError(409, "block history may only be removed by explicit wipe", "revision_conflict");
|
|
447
|
-
const multi = multiEntryIndex(def);
|
|
448
|
-
if (!isRange(keyOrRange)) {
|
|
449
|
-
const parts = toParts(keyOrRange);
|
|
450
|
-
if (parts.length > def.keyPath.length) {
|
|
451
|
-
throw new StorageError(400, `key arity ${parts.length} > keyPath arity ${def.keyPath.length} for ${def.name}`);
|
|
452
|
-
}
|
|
453
|
-
const n = Math.min(parts.length, def.keyPath.length);
|
|
454
|
-
const cond = keyCols(def).slice(0, n).map((c) => `"${c}" = ?`).join(" AND ");
|
|
455
|
-
const params2 = parts.slice(0, n).map(toSqlScalar);
|
|
456
|
-
if (multi) await conn.exec(`DELETE FROM "${junctionTable(def, multi)}" WHERE ${cond}`, params2);
|
|
457
|
-
await conn.exec(`DELETE FROM "${def.name}" WHERE ${cond}`, params2);
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
validateRange(keyOrRange);
|
|
461
|
-
const cols = keyCols(def);
|
|
462
|
-
const params = [];
|
|
463
|
-
const where = rangeWhere(cols, keyOrRange, params);
|
|
464
|
-
if (where === null) {
|
|
465
|
-
for (const row of await listRows(conn, def, keyOrRange)) {
|
|
466
|
-
await deleteRows(conn, def, requireKey(def, row));
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
const collist = cols.map((c) => `"${c}"`).join(", ");
|
|
471
|
-
if (multi) {
|
|
472
|
-
await conn.exec(
|
|
473
|
-
`DELETE FROM "${junctionTable(def, multi)}" WHERE (${collist}) IN (SELECT ${collist} FROM "${def.name}"${where})`,
|
|
474
|
-
params
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
await conn.exec(`DELETE FROM "${def.name}"${where}`, params);
|
|
478
|
-
}
|
|
479
|
-
async function clearRows(conn, def, wipe = false) {
|
|
480
|
-
if (def.name === "block_revisions" && !wipe) throw new StorageError(409, "block history may only be removed by explicit wipe", "revision_conflict");
|
|
481
|
-
const multi = multiEntryIndex(def);
|
|
482
|
-
if (multi) await conn.exec(`DELETE FROM "${junctionTable(def, multi)}"`);
|
|
483
|
-
await conn.exec(`DELETE FROM "${def.name}"`);
|
|
484
|
-
}
|
|
485
|
-
async function wipeRows(conn) {
|
|
486
|
-
for (const def of STORE_DEFS) {
|
|
487
|
-
if (!def.system) await clearRows(conn, def, true);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
function tableListSql(dialect) {
|
|
491
|
-
return dialect === "sqlite" ? `SELECT "name" FROM "sqlite_master" WHERE "type" = 'table'` : `SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = current_schema()`;
|
|
492
|
-
}
|
|
493
|
-
async function ensureStoreTables(conn, dialect, def) {
|
|
494
|
-
const keys = keyCols(def);
|
|
495
|
-
const cols = keys.map((k, i) => [k, colSql(dialect, pathType(def, def.keyPath[i]))]);
|
|
496
|
-
for (const idx of def.indexes) {
|
|
497
|
-
if (idx.multiEntry) continue;
|
|
498
|
-
idx.keyPath.forEach((path, part) => {
|
|
499
|
-
cols.push([indexCol(idx.name, part), colSql(dialect, pathType(def, path))]);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
cols.push(["data", " TEXT NOT NULL"]);
|
|
503
|
-
const colDefs = cols.map(([c, t]) => `"${c}"${t}`);
|
|
504
|
-
await conn.exec(
|
|
505
|
-
`CREATE TABLE IF NOT EXISTS "${def.name}" (${colDefs.join(", ")}, PRIMARY KEY (${keys.map((k) => `"${k}"`).join(", ")}))`
|
|
506
|
-
);
|
|
507
|
-
for (const idx of def.indexes) {
|
|
508
|
-
if (idx.multiEntry) continue;
|
|
509
|
-
const parts = idx.keyPath.map((_, part) => `"${indexCol(idx.name, part)}"`).join(", ");
|
|
510
|
-
await conn.exec(
|
|
511
|
-
`CREATE ${idx.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS "${def.name}__${sanitize(idx.name)}" ON "${def.name}" (${parts})`
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
const multi = multiEntryIndex(def);
|
|
515
|
-
if (multi) {
|
|
516
|
-
const jt = junctionTable(def, multi);
|
|
517
|
-
const jkeys = keys.map((k, i) => `"${k}"${colSql(dialect, pathType(def, def.keyPath[i]))}`);
|
|
518
|
-
await conn.exec(
|
|
519
|
-
`CREATE TABLE IF NOT EXISTS "${jt}" (${jkeys.join(", ")}, "v"${colSql(dialect, "text")}, PRIMARY KEY (${[...keys, '"v"'].join(", ")}))`
|
|
520
|
-
);
|
|
521
|
-
await conn.exec(`CREATE INDEX IF NOT EXISTS "${jt}__v" ON "${jt}" ("v")`);
|
|
522
|
-
}
|
|
523
|
-
if (def.name === "notes" && dialect === "sqlite") {
|
|
524
|
-
await conn.exec(`CREATE TRIGGER IF NOT EXISTS "notes_revision_guard" BEFORE UPDATE ON "notes"
|
|
525
|
-
WHEN json_extract(NEW."data", '$.sourceRevision') < json_extract(OLD."data", '$.sourceRevision')
|
|
526
|
-
OR ((json_extract(NEW."data", '$.rawContent') IS NOT json_extract(OLD."data", '$.rawContent')
|
|
527
|
-
OR json_extract(NEW."data", '$.blocks') IS NOT json_extract(OLD."data", '$.blocks'))
|
|
528
|
-
AND json_extract(NEW."data", '$.sourceRevision') <= json_extract(OLD."data", '$.sourceRevision'))
|
|
529
|
-
BEGIN SELECT RAISE(ABORT, 'transaction_conflict: stale Note source revision'); END`);
|
|
530
|
-
}
|
|
531
|
-
if (def.name === "block_revisions" && dialect === "sqlite") {
|
|
532
|
-
await conn.exec(`CREATE TRIGGER IF NOT EXISTS "block_revision_guard" BEFORE INSERT ON "block_revisions"
|
|
533
|
-
WHEN EXISTS (SELECT 1 FROM "block_revisions" old WHERE old."k0" = NEW."k0" AND old."k1" = NEW."k1" AND old."k2" = NEW."k2" AND old."data" <> NEW."data")
|
|
534
|
-
BEGIN SELECT RAISE(ABORT, 'revision_conflict: immutable block revision collision'); END`);
|
|
535
|
-
}
|
|
536
|
-
if (def.name === "sessions" && dialect === "sqlite") {
|
|
537
|
-
await conn.exec(`CREATE TRIGGER IF NOT EXISTS "session_pin_guard" BEFORE UPDATE ON "sessions"
|
|
538
|
-
WHEN json_extract(NEW."data", '$.noteId') IS NOT json_extract(OLD."data", '$.noteId')
|
|
539
|
-
OR json_extract(NEW."data", '$.blockId') IS NOT json_extract(OLD."data", '$.blockId')
|
|
540
|
-
OR json_extract(NEW."data", '$.blockVersion') IS NOT json_extract(OLD."data", '$.blockVersion')
|
|
541
|
-
BEGIN SELECT RAISE(ABORT, 'invalid_session_pin: Session pin is immutable'); END`);
|
|
542
|
-
}
|
|
543
|
-
if (dialect === "postgres" && ["notes", "block_revisions", "sessions"].includes(def.name)) {
|
|
544
|
-
const condition = def.name === "block_revisions" ? `EXISTS (SELECT 1 FROM "block_revisions" saved WHERE saved."k0" = NEW."k0" AND saved."k1" = NEW."k1" AND saved."k2" = NEW."k2" AND saved."data" <> NEW."data")` : def.name === "notes" ? `(NEW."data"::jsonb->>'sourceRevision')::bigint < (OLD."data"::jsonb->>'sourceRevision')::bigint OR ((NEW."data"::jsonb->'rawContent' IS DISTINCT FROM OLD."data"::jsonb->'rawContent' OR NEW."data"::jsonb->'blocks' IS DISTINCT FROM OLD."data"::jsonb->'blocks') AND (NEW."data"::jsonb->>'sourceRevision')::bigint <= (OLD."data"::jsonb->>'sourceRevision')::bigint)` : `NEW."data"::jsonb->'noteId' IS DISTINCT FROM OLD."data"::jsonb->'noteId' OR NEW."data"::jsonb->'blockId' IS DISTINCT FROM OLD."data"::jsonb->'blockId' OR NEW."data"::jsonb->'blockVersion' IS DISTINCT FROM OLD."data"::jsonb->'blockVersion'`;
|
|
545
|
-
const code = def.name === "block_revisions" ? "revision_conflict" : def.name === "notes" ? "transaction_conflict" : "invalid_session_pin";
|
|
546
|
-
const guard = `${def.name}_guard`;
|
|
547
|
-
await conn.exec(`CREATE OR REPLACE FUNCTION "${guard}"() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF ${condition} THEN RAISE EXCEPTION '${code}'; END IF; RETURN NEW; END; $$`);
|
|
548
|
-
await conn.exec(`DROP TRIGGER IF EXISTS "${guard}" ON "${def.name}"`);
|
|
549
|
-
await conn.exec(`CREATE TRIGGER "${guard}" BEFORE ${def.name === "block_revisions" ? "INSERT" : "UPDATE"} ON "${def.name}" FOR EACH ROW EXECUTE FUNCTION "${guard}"()`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
async function initializeStore(db) {
|
|
553
|
-
await db.transaction(async (tx) => {
|
|
554
|
-
await tx.exec(
|
|
555
|
-
`CREATE TABLE IF NOT EXISTS "${SCHEMA_TABLE}" ("key" TEXT PRIMARY KEY, "value" TEXT NOT NULL)`
|
|
556
|
-
);
|
|
557
|
-
const ignored = (name) => name.startsWith("_wodwiki_") || name.startsWith("sqlite_") || name === "domain_note" || name === "query_block";
|
|
558
|
-
const storeTables = new Set(STORE_DEFS.map((d) => d.name));
|
|
559
|
-
for (const def of STORE_DEFS) {
|
|
560
|
-
const multi = def.indexes.find((i) => i.multiEntry);
|
|
561
|
-
if (multi) storeTables.add(junctionTable(def, multi));
|
|
562
|
-
}
|
|
563
|
-
const tableNames = new Set(
|
|
564
|
-
(await tx.query(tableListSql(db.dialect))).map((r) => String(Object.values(r)[0])).filter((n) => !ignored(n))
|
|
565
|
-
);
|
|
566
|
-
const guardRows = await tx.query(`SELECT "value" FROM "${SCHEMA_TABLE}" WHERE "key" = ?`, [SCHEMA_KEY]);
|
|
567
|
-
let stored;
|
|
568
|
-
if (guardRows[0]) {
|
|
569
|
-
const guard = JSON.parse(String(guardRows[0].value));
|
|
570
|
-
if (typeof guard.version !== "number" || !Number.isSafeInteger(guard.version) || guard.version < 0) {
|
|
571
|
-
throw new StorageError(500, `corrupt ${SCHEMA_TABLE} guard row`, "bad_schema_guard");
|
|
572
|
-
}
|
|
573
|
-
stored = guard.version;
|
|
574
|
-
if (stored > DB_VERSION) {
|
|
575
|
-
throw new StorageError(
|
|
576
|
-
500,
|
|
577
|
-
`database schema v${stored} is newer than this server supports (v${DB_VERSION}); refusing to downgrade`,
|
|
578
|
-
"schema_incompatible"
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
if (stored < DB_VERSION) {
|
|
582
|
-
const missing = [...storeTables].filter((name) => !tableNames.has(name));
|
|
583
|
-
const unexpected = [...tableNames].filter((name) => name !== SCHEMA_TABLE && !storeTables.has(name));
|
|
584
|
-
const additive = additiveStoresSince(stored);
|
|
585
|
-
if (stored < 30 || unexpected.length > 0 || missing.some((name) => !additive.has(name))) {
|
|
586
|
-
throw new StorageError(
|
|
587
|
-
500,
|
|
588
|
-
`database schema v${stored} is incompatible with v${DB_VERSION} (missing tables: ${missing.join(", ") || "none"}; unrecognized tables: ${unexpected.join(", ") || "none"}); recreation required using an explicitly disposable database`,
|
|
589
|
-
"schema_incompatible"
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
} else if ([...tableNames].some((name) => storeTables.has(name))) {
|
|
594
|
-
throw new StorageError(
|
|
595
|
-
500,
|
|
596
|
-
"database contains store tables but no wod-wiki schema guard; refusing to adopt a foreign database \u2014 point SQLITE_PATH/DATABASE_URL at a fresh database",
|
|
597
|
-
"foreign_database"
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
for (const def of STORE_DEFS) await ensureStoreTables(tx, db.dialect, def);
|
|
601
|
-
if (stored !== DB_VERSION) {
|
|
602
|
-
await tx.exec(
|
|
603
|
-
`INSERT INTO "${SCHEMA_TABLE}" ("key", "value") VALUES (?, ?) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value"`,
|
|
604
|
-
[SCHEMA_KEY, JSON.stringify({ version: DB_VERSION })]
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
12
|
var TABLES = ["domain_note", "query_block"];
|
|
610
13
|
var PROJECTION_GUARD_KEY = "domain_projection";
|
|
611
14
|
function sourceItem(row) {
|
|
@@ -1600,291 +1003,224 @@ var BrowserTursoStorage = class extends SqlStorage {
|
|
|
1600
1003
|
}
|
|
1601
1004
|
};
|
|
1602
1005
|
|
|
1603
|
-
// src/sql/
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
}
|
|
1621
|
-
function
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
assertKey(mutation.key, "sync put key");
|
|
1638
|
-
if (mutation.value === null || typeof mutation.value !== "object" || Array.isArray(mutation.value)) {
|
|
1639
|
-
throw new StorageError(400, "sync put value must be an object row", "bad_sync_mutation");
|
|
1640
|
-
}
|
|
1641
|
-
return { op: "put", store, key: mutation.key, value: mutation.value };
|
|
1642
|
-
}
|
|
1643
|
-
throw new StorageError(400, `sync mutation has unknown op ${String(mutation.op)}`, "bad_sync_mutation");
|
|
1644
|
-
}
|
|
1645
|
-
function validateEntry(entry) {
|
|
1646
|
-
if (typeof entry.id !== "string" || entry.id.length === 0 || entry.id.length > 200) {
|
|
1647
|
-
throw new StorageError(400, "sync entry id must be a non-empty string", "bad_sync_entry");
|
|
1648
|
-
}
|
|
1649
|
-
if (!isSafeInt(entry.at) || entry.at < 0) {
|
|
1650
|
-
throw new StorageError(400, "sync entry at must be a non-negative safe integer", "bad_sync_entry");
|
|
1651
|
-
}
|
|
1652
|
-
if (!Array.isArray(entry.ops) || entry.ops.length === 0) {
|
|
1653
|
-
throw new StorageError(400, "sync entry ops must be a non-empty array", "bad_sync_entry");
|
|
1654
|
-
}
|
|
1655
|
-
for (const op of entry.ops) validateMutation(op);
|
|
1656
|
-
}
|
|
1657
|
-
async function runStatements(tx, statements) {
|
|
1658
|
-
if (statements.length === 0) return [];
|
|
1659
|
-
if (tx.batch) return tx.batch(statements);
|
|
1660
|
-
const results = [];
|
|
1661
|
-
for (const statement of statements) {
|
|
1662
|
-
results.push(await tx.query(statement.sql, statement.params ?? []));
|
|
1006
|
+
// src/sql/backup.ts
|
|
1007
|
+
function bytesToHex(bytes) {
|
|
1008
|
+
let hex = "";
|
|
1009
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
1010
|
+
return hex;
|
|
1011
|
+
}
|
|
1012
|
+
function toCanonicalCell(value) {
|
|
1013
|
+
if (value === null) return { z: null };
|
|
1014
|
+
if (typeof value === "string") return { s: value };
|
|
1015
|
+
if (typeof value === "number") return { n: value };
|
|
1016
|
+
if (typeof value === "bigint") return { i: value.toString() };
|
|
1017
|
+
if (value instanceof Uint8Array) return { b: bytesToHex(value) };
|
|
1018
|
+
if (value instanceof ArrayBuffer) return { b: bytesToHex(new Uint8Array(value)) };
|
|
1019
|
+
throw new StorageError(500, `fingerprint cannot encode cell of type ${typeof value}`, "bad_fingerprint_cell");
|
|
1020
|
+
}
|
|
1021
|
+
function quoteIdent(name) {
|
|
1022
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
1023
|
+
}
|
|
1024
|
+
async function sha256(text2) {
|
|
1025
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1026
|
+
if (!subtle) throw new StorageError(500, "Web Crypto unavailable \u2014 cannot fingerprint the database", "no_webcrypto");
|
|
1027
|
+
const bytes = new Uint8Array(await subtle.digest("SHA-256", new TextEncoder().encode(text2)));
|
|
1028
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1029
|
+
}
|
|
1030
|
+
async function readStateMarker(conn) {
|
|
1031
|
+
const tables = await conn.query(
|
|
1032
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
1033
|
+
[SYNC_STATE_TABLE]
|
|
1034
|
+
);
|
|
1035
|
+
if (tables.length === 0) return void 0;
|
|
1036
|
+
const rows = await conn.query(`SELECT "value" FROM ${quoteIdent(SYNC_STATE_TABLE)} WHERE "key" = 'meta'`);
|
|
1037
|
+
const raw = rows[0]?.value;
|
|
1038
|
+
if (typeof raw !== "string") {
|
|
1039
|
+
throw new StorageError(400, "sync target is not initialized", "sync_not_initialized");
|
|
1663
1040
|
}
|
|
1664
|
-
return results;
|
|
1665
|
-
}
|
|
1666
|
-
async function tableExists(conn, dialect, name) {
|
|
1667
|
-
const sql = dialect === "postgres" ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1` : `SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`;
|
|
1668
|
-
return (await conn.query(sql, [name])).length > 0;
|
|
1669
|
-
}
|
|
1670
|
-
async function readSyncState(tx) {
|
|
1671
|
-
const rows = await tx.query(`SELECT "value" FROM "${SYNC_STATE_TABLE}" WHERE "key" = ?`, [STATE_KEY]);
|
|
1672
|
-
if (!rows[0]) throw new StorageError(409, "sync target is not initialized", "sync_not_initialized");
|
|
1673
1041
|
let parsed;
|
|
1674
1042
|
try {
|
|
1675
|
-
parsed = JSON.parse(
|
|
1043
|
+
parsed = JSON.parse(raw);
|
|
1676
1044
|
} catch {
|
|
1677
1045
|
throw new StorageError(400, "malformed sync state row", "bad_sync_state");
|
|
1678
1046
|
}
|
|
1679
1047
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1680
1048
|
throw new StorageError(400, "malformed sync state row", "bad_sync_state");
|
|
1681
1049
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
throw new StorageError(400, "sync state schema/rev must be non-negative safe integers", "bad_sync_state");
|
|
1050
|
+
if (!("schema" in parsed) || !("rev" in parsed)) {
|
|
1051
|
+
throw new StorageError(400, "sync state row is missing schema/rev", "bad_sync_state");
|
|
1685
1052
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
if (await tableExists(db, db.dialect, SYNC_STATE_TABLE)) {
|
|
1690
|
-
const state = await readSyncState(db);
|
|
1691
|
-
return { kind: "current", schemaVersion: state.schema, rev: state.rev };
|
|
1692
|
-
}
|
|
1693
|
-
if (await tableExists(db, db.dialect, SCHEMA_TABLE)) {
|
|
1694
|
-
const rows = await db.query(`SELECT "value" FROM "${SCHEMA_TABLE}" WHERE "key" = 'schema'`);
|
|
1695
|
-
if (rows[0]) {
|
|
1696
|
-
const guard = JSON.parse(String(rows[0].value));
|
|
1697
|
-
if (typeof guard.version === "number" && Number.isSafeInteger(guard.version) && guard.version >= 0) {
|
|
1698
|
-
return { kind: "legacy", schemaVersion: guard.version };
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
return { kind: "legacy", schemaVersion: 0 };
|
|
1053
|
+
const { schema, rev } = parsed;
|
|
1054
|
+
if (typeof schema !== "number" || !Number.isSafeInteger(schema) || schema < 0 || typeof rev !== "number" || !Number.isSafeInteger(rev) || rev < 0) {
|
|
1055
|
+
throw new StorageError(400, "sync state schema/rev must be non-negative safe integers", "bad_sync_state");
|
|
1702
1056
|
}
|
|
1703
|
-
|
|
1704
|
-
db.dialect === "postgres" ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'` : `SELECT name FROM sqlite_master WHERE type = 'table'`
|
|
1705
|
-
);
|
|
1706
|
-
const internal = (name) => name.startsWith("_wodwiki_") || name.startsWith("sqlite_");
|
|
1707
|
-
return others.some((row) => !internal(String(Object.values(row)[0]))) ? { kind: "unknown" } : "empty";
|
|
1057
|
+
return { schema, rev };
|
|
1708
1058
|
}
|
|
1709
|
-
async function
|
|
1710
|
-
|
|
1711
|
-
await tx.
|
|
1712
|
-
|
|
1713
|
-
`CREATE TABLE IF NOT EXISTS "${SYNC_OPS_TABLE}" ("op_id" TEXT PRIMARY KEY, "rev" INTEGER NOT NULL, "idx" INTEGER NOT NULL, "at" INTEGER NOT NULL, "data" TEXT NOT NULL)`
|
|
1714
|
-
);
|
|
1715
|
-
await tx.exec(
|
|
1716
|
-
`CREATE INDEX IF NOT EXISTS "${SYNC_OPS_TABLE}_rev" ON "${SYNC_OPS_TABLE}" ("rev", "idx")`
|
|
1717
|
-
);
|
|
1718
|
-
await tx.exec(
|
|
1719
|
-
`INSERT INTO "${SYNC_STATE_TABLE}" ("key", "value") VALUES (?, ?) ON CONFLICT ("key") DO NOTHING`,
|
|
1720
|
-
[STATE_KEY, JSON.stringify({ schema: schemaVersion, rev: 0 })]
|
|
1059
|
+
async function databaseFingerprint(db) {
|
|
1060
|
+
return db.transaction(async (tx) => {
|
|
1061
|
+
const tableRows = await tx.query(
|
|
1062
|
+
`SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND name NOT LIKE 'libsql\\_%' ESCAPE '\\'`
|
|
1721
1063
|
);
|
|
1064
|
+
const tables = tableRows.map((row) => ({ name: String(row.name), createSql: typeof row.sql === "string" ? row.sql : "" })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1065
|
+
const sections = ["wod-wiki-db-fingerprint-v2"];
|
|
1066
|
+
let totalRows = 0;
|
|
1067
|
+
for (const table of tables) {
|
|
1068
|
+
const rows = await tx.query(`SELECT * FROM ${quoteIdent(table.name)}`);
|
|
1069
|
+
totalRows += rows.length;
|
|
1070
|
+
const encoded = rows.map((row) => JSON.stringify(Object.values(row).map(toCanonicalCell)));
|
|
1071
|
+
encoded.sort();
|
|
1072
|
+
sections.push(`table ${table.name}`, `schema ${table.createSql}`, `rows ${rows.length}`, ...encoded);
|
|
1073
|
+
}
|
|
1074
|
+
const sync = await readStateMarker(tx);
|
|
1075
|
+
return { tables: tables.length, rows: totalRows, digest: await sha256(sections.join("\n")), sync };
|
|
1722
1076
|
});
|
|
1723
1077
|
}
|
|
1724
|
-
function
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1078
|
+
async function assertDatabaseCopyMatches(source, copy, before) {
|
|
1079
|
+
const after = await databaseFingerprint(source);
|
|
1080
|
+
if (after.digest !== before.digest) {
|
|
1081
|
+
throw new StorageError(
|
|
1082
|
+
409,
|
|
1083
|
+
"source database changed during the copy \u2014 a writer is still active; retire all writers and retry",
|
|
1084
|
+
"source_moved"
|
|
1085
|
+
);
|
|
1732
1086
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1087
|
+
const copied = await databaseFingerprint(copy);
|
|
1088
|
+
if (copied.digest !== before.digest) {
|
|
1089
|
+
throw new StorageError(
|
|
1090
|
+
409,
|
|
1091
|
+
`copy is incomplete or divergent (copy: ${copied.tables} tables / ${copied.rows} rows, source: ${before.tables} tables / ${before.rows} rows)`,
|
|
1092
|
+
"copy_mismatch"
|
|
1093
|
+
);
|
|
1738
1094
|
}
|
|
1739
|
-
|
|
1740
|
-
if (mutation.op !== "put") return mutation;
|
|
1741
|
-
return {
|
|
1742
|
-
op: "put",
|
|
1743
|
-
store: mutation.store,
|
|
1744
|
-
key: mutation.key,
|
|
1745
|
-
value: decodeRowJson(JSON.stringify(mutation.value), BINARY_FIELD2[mutation.store])
|
|
1746
|
-
};
|
|
1095
|
+
return after;
|
|
1747
1096
|
}
|
|
1748
|
-
async function
|
|
1749
|
-
if (
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1097
|
+
async function finalizeMigrationTarget(db, toSchema, expectRev) {
|
|
1098
|
+
if (!Number.isSafeInteger(toSchema) || toSchema < 1) {
|
|
1099
|
+
throw new StorageError(400, "finalize requires a positive schema version", "bad_sync_state");
|
|
1100
|
+
}
|
|
1101
|
+
await db.transaction(async (tx) => {
|
|
1102
|
+
const marker = await readStateMarker(tx);
|
|
1103
|
+
if (!marker || marker.schema !== 0 || marker.rev !== expectRev) {
|
|
1754
1104
|
throw new StorageError(
|
|
1755
1105
|
409,
|
|
1756
|
-
`
|
|
1757
|
-
"
|
|
1106
|
+
`migration target is not in constructing state (expected schema 0 at rev ${expectRev}, found ${marker ? `schema ${marker.schema} at rev ${marker.rev}` : "no state row"})`,
|
|
1107
|
+
"migration_not_constructing"
|
|
1758
1108
|
);
|
|
1759
1109
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1110
|
+
await tx.exec(
|
|
1111
|
+
`UPDATE ${quoteIdent(SYNC_STATE_TABLE)} SET "value" = ? WHERE "key" = 'meta'`,
|
|
1112
|
+
[JSON.stringify({ schema: toSchema, rev: expectRev })]
|
|
1113
|
+
);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
function pinKey(noteId, blockId, version) {
|
|
1117
|
+
return `${String(noteId)}\0${String(blockId)}\0${String(version)}`;
|
|
1118
|
+
}
|
|
1119
|
+
function assertPolicySnapshot(ops) {
|
|
1120
|
+
const revisions = /* @__PURE__ */ new Set();
|
|
1121
|
+
for (const mutation of ops) {
|
|
1122
|
+
if (mutation.op !== "put") {
|
|
1123
|
+
throw new StorageError(
|
|
1124
|
+
400,
|
|
1125
|
+
`policy snapshot contains a non-put operation (${mutation.op}) \u2014 refusing`,
|
|
1126
|
+
"policy_violation"
|
|
1767
1127
|
);
|
|
1768
|
-
for (const row of rows) present.add(String(row.op_id));
|
|
1769
1128
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
if (present.has(`${entry.id}:0`)) continue;
|
|
1775
|
-
rev += 1;
|
|
1776
|
-
committed += 1;
|
|
1777
|
-
entry.ops.forEach((mutation, idx) => {
|
|
1778
|
-
statements.push({
|
|
1779
|
-
sql: `INSERT INTO "${SYNC_OPS_TABLE}" ("op_id", "rev", "idx", "at", "data") VALUES (?, ?, ?, ?, ?) ON CONFLICT ("op_id") DO NOTHING`,
|
|
1780
|
-
params: [`${entry.id}:${idx}`, rev, idx, entry.at, encodeMutation(mutation)]
|
|
1781
|
-
});
|
|
1782
|
-
});
|
|
1129
|
+
if (mutation.store !== "block_revisions") continue;
|
|
1130
|
+
const row = mutation.value;
|
|
1131
|
+
if (typeof row !== "object" || row === null || !("noteId" in row) || !("blockId" in row) || !("version" in row)) {
|
|
1132
|
+
throw new StorageError(400, "block_revisions row is missing noteId/blockId/version", "policy_violation");
|
|
1783
1133
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
async function fetchSyncOps(db, afterRev, limit) {
|
|
1794
|
-
if (!isSafeInt(afterRev) || afterRev < 0) {
|
|
1795
|
-
throw new StorageError(400, "fetchSyncOps cursor must be a non-negative safe integer", "bad_sync_cursor");
|
|
1796
|
-
}
|
|
1797
|
-
const bounded = Math.min(Math.max(1, Math.floor(limit)), 4294967295);
|
|
1798
|
-
const revRows = await db.query(
|
|
1799
|
-
`SELECT DISTINCT "rev" FROM (SELECT "rev" FROM "${SYNC_OPS_TABLE}" WHERE "rev" > ? ORDER BY "rev" ASC LIMIT ?)`,
|
|
1800
|
-
[afterRev, bounded]
|
|
1801
|
-
);
|
|
1802
|
-
const revs = revRows.map((row) => {
|
|
1803
|
-
const rev = Number(row.rev);
|
|
1804
|
-
if (!Number.isSafeInteger(rev) || rev < 0) {
|
|
1805
|
-
throw new StorageError(400, "sync op rev must be a non-negative safe integer", "bad_sync_row");
|
|
1134
|
+
revisions.add(pinKey(row.noteId, row.blockId, row.version));
|
|
1135
|
+
}
|
|
1136
|
+
const check = (noteId, blockId, version, what) => {
|
|
1137
|
+
if (!revisions.has(pinKey(noteId, blockId, version))) {
|
|
1138
|
+
throw new StorageError(
|
|
1139
|
+
400,
|
|
1140
|
+
`${what} pins [${String(noteId)}, ${String(blockId)}, ${String(version)}] but the snapshot has no such immutable revision`,
|
|
1141
|
+
"policy_violation"
|
|
1142
|
+
);
|
|
1806
1143
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1144
|
+
};
|
|
1145
|
+
for (const mutation of ops) {
|
|
1146
|
+
if (mutation.op !== "put") continue;
|
|
1147
|
+
if (mutation.store === "sessions") {
|
|
1148
|
+
const row = mutation.value;
|
|
1149
|
+
if (typeof row !== "object" || row === null || !("noteId" in row) || !("blockId" in row) || !("blockVersion" in row)) {
|
|
1150
|
+
throw new StorageError(400, "session row is missing noteId/blockId/blockVersion", "policy_violation");
|
|
1151
|
+
}
|
|
1152
|
+
check(row.noteId, row.blockId, row.blockVersion, "a session");
|
|
1153
|
+
} else if (mutation.store === "notes") {
|
|
1154
|
+
const row = mutation.value;
|
|
1155
|
+
if (typeof row !== "object" || row === null || !("id" in row) || !("blocks" in row)) {
|
|
1156
|
+
throw new StorageError(400, "note row is missing id/blocks", "policy_violation");
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof row.blocks !== "object" || row.blocks === null) continue;
|
|
1159
|
+
for (const block of Object.values(row.blocks)) {
|
|
1160
|
+
if (typeof block !== "object" || block === null || !("id" in block) || !("version" in block)) {
|
|
1161
|
+
throw new StorageError(400, "note block entry is missing id/version", "policy_violation");
|
|
1162
|
+
}
|
|
1163
|
+
check(row.id, block.id, block.version, "a note block");
|
|
1164
|
+
}
|
|
1821
1165
|
}
|
|
1822
|
-
|
|
1823
|
-
});
|
|
1166
|
+
}
|
|
1824
1167
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
for (const entry of entries) validateEntry(entry);
|
|
1828
|
-
await db.transaction(async (tx) => {
|
|
1829
|
-
await readSyncState(tx);
|
|
1830
|
-
await tx.exec(
|
|
1831
|
-
`CREATE TABLE IF NOT EXISTS "${SYNC_ARCHIVE_TABLE}" ("op_id" TEXT PRIMARY KEY, "rev" INTEGER NOT NULL, "idx" INTEGER NOT NULL, "at" INTEGER NOT NULL, "data" TEXT NOT NULL)`
|
|
1832
|
-
);
|
|
1833
|
-
await tx.exec(`INSERT INTO "${SYNC_ARCHIVE_TABLE}" ("op_id", "rev", "idx", "at", "data") SELECT "op_id", "rev", "idx", "at", "data" FROM "${SYNC_OPS_TABLE}"`);
|
|
1834
|
-
await tx.exec(`DELETE FROM "${SYNC_OPS_TABLE}"`);
|
|
1835
|
-
const statements = [];
|
|
1836
|
-
let rev = 0;
|
|
1837
|
-
for (const entry of entries) {
|
|
1838
|
-
rev += 1;
|
|
1839
|
-
entry.ops.forEach((mutation, idx) => {
|
|
1840
|
-
statements.push({
|
|
1841
|
-
sql: `INSERT INTO "${SYNC_OPS_TABLE}" ("op_id", "rev", "idx", "at", "data") VALUES (?, ?, ?, ?, ?) ON CONFLICT ("op_id") DO NOTHING`,
|
|
1842
|
-
params: [`${entry.id}:${idx}`, rev, idx, entry.at, encodeMutation(mutation)]
|
|
1843
|
-
});
|
|
1844
|
-
});
|
|
1845
|
-
}
|
|
1846
|
-
statements.push({
|
|
1847
|
-
sql: `UPDATE "${SYNC_STATE_TABLE}" SET "value" = ? WHERE "key" = ?`,
|
|
1848
|
-
params: [JSON.stringify({ schema: toSchemaVersion, rev }), STATE_KEY]
|
|
1849
|
-
});
|
|
1850
|
-
await runStatements(tx, statements);
|
|
1851
|
-
});
|
|
1168
|
+
function logKey(store, key) {
|
|
1169
|
+
return `${store}\0${JSON.stringify(key) ?? String(key)}`;
|
|
1852
1170
|
}
|
|
1853
|
-
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1171
|
+
function assertLogPinClosure(ops) {
|
|
1172
|
+
const live = /* @__PURE__ */ new Map();
|
|
1173
|
+
for (const { mutation } of ops) {
|
|
1174
|
+
if (mutation.op === "put") {
|
|
1175
|
+
live.set(logKey(mutation.store, mutation.key), { store: mutation.store, value: mutation.value });
|
|
1176
|
+
} else if (mutation.op === "delete") {
|
|
1177
|
+
live.delete(logKey(mutation.store, mutation.key));
|
|
1178
|
+
} else if (mutation.op === "clear") {
|
|
1179
|
+
for (const key of live.keys()) {
|
|
1180
|
+
if (key.startsWith(`${mutation.store}\0`)) live.delete(key);
|
|
1181
|
+
}
|
|
1182
|
+
} else {
|
|
1183
|
+
live.clear();
|
|
1860
1184
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1185
|
+
}
|
|
1186
|
+
const revisions = /* @__PURE__ */ new Set();
|
|
1187
|
+
for (const row of live.values()) {
|
|
1188
|
+
if (row.store !== "block_revisions") continue;
|
|
1189
|
+
if (!("noteId" in row.value) || !("blockId" in row.value) || !("version" in row.value)) {
|
|
1190
|
+
throw new StorageError(400, "block_revisions row is missing noteId/blockId/version", "policy_violation");
|
|
1866
1191
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1192
|
+
revisions.add(pinKey(row.value.noteId, row.value.blockId, row.value.version));
|
|
1193
|
+
}
|
|
1194
|
+
const check = (noteId, blockId, version, what) => {
|
|
1195
|
+
if (!revisions.has(pinKey(noteId, blockId, version))) {
|
|
1196
|
+
throw new StorageError(
|
|
1197
|
+
400,
|
|
1198
|
+
`${what} pins [${String(noteId)}, ${String(blockId)}, ${String(version)}] but the target has no live immutable revision for it`,
|
|
1199
|
+
"policy_violation"
|
|
1200
|
+
);
|
|
1869
1201
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1202
|
+
};
|
|
1203
|
+
for (const row of live.values()) {
|
|
1204
|
+
if (row.store === "sessions") {
|
|
1205
|
+
if (!("noteId" in row.value) || !("blockId" in row.value) || !("blockVersion" in row.value)) {
|
|
1206
|
+
throw new StorageError(400, "session row is missing noteId/blockId/blockVersion", "policy_violation");
|
|
1207
|
+
}
|
|
1208
|
+
check(row.value.noteId, row.value.blockId, row.value.blockVersion, "a live session");
|
|
1209
|
+
} else if (row.store === "notes") {
|
|
1210
|
+
if (!("id" in row.value) || !("blocks" in row.value)) {
|
|
1211
|
+
throw new StorageError(400, "note row is missing id/blocks", "policy_violation");
|
|
1212
|
+
}
|
|
1213
|
+
if (typeof row.value.blocks !== "object" || row.value.blocks === null) continue;
|
|
1214
|
+
for (const block of Object.values(row.value.blocks)) {
|
|
1215
|
+
if (typeof block !== "object" || block === null || !("id" in block) || !("version" in block)) {
|
|
1216
|
+
throw new StorageError(400, "note block entry is missing id/version", "policy_violation");
|
|
1217
|
+
}
|
|
1218
|
+
check(row.value.id, block.id, block.version, "a live note block");
|
|
1219
|
+
}
|
|
1873
1220
|
}
|
|
1874
|
-
|
|
1875
|
-
op: mutation.op,
|
|
1876
|
-
store: mutation.store,
|
|
1877
|
-
key: mutation.key,
|
|
1878
|
-
value: mutation.value === void 0 || mutation.value === null ? void 0 : decodeRowJson(JSON.stringify(mutation.value), BINARY_FIELD2[mutation.store])
|
|
1879
|
-
};
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
async function readLegacyRows(db, table) {
|
|
1883
|
-
if (!await tableExists(db, db.dialect, table)) return [];
|
|
1884
|
-
const rows = await db.query(`SELECT "data" FROM "${table}"`);
|
|
1885
|
-
return rows.map((row) => decodeRowJson(String(row.data), BINARY_FIELD2[table]));
|
|
1221
|
+
}
|
|
1886
1222
|
}
|
|
1887
1223
|
|
|
1888
|
-
export { BrowserTursoStorage, MAX_BODY,
|
|
1224
|
+
export { BrowserTursoStorage, MAX_BODY, SqlStorage, assertDatabaseCopyMatches, assertLogPinClosure, assertPolicySnapshot, databaseFingerprint, finalizeMigrationTarget, handleDomainQuery, initializeDomain, rebuildProjections, refreshDomain, validateTursoUrl, verifyReadiness, wipeDomain };
|
|
1889
1225
|
//# sourceMappingURL=index.mjs.map
|
|
1890
1226
|
//# sourceMappingURL=index.mjs.map
|