terrascale 0.3.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/LICENSE +202 -0
- package/README.md +143 -0
- package/package.json +159 -0
- package/sdk-current-contract.json +27 -0
- package/sdk-route-manifest.json +67 -0
- package/src/admin.js +9 -0
- package/src/better-auth.js +14 -0
- package/src/config.js +121 -0
- package/src/database-codec.js +845 -0
- package/src/database-types.js +237 -0
- package/src/database-view.js +422 -0
- package/src/database.js +420 -0
- package/src/discovery.js +374 -0
- package/src/http.js +887 -0
- package/src/index.js +79 -0
- package/src/local/authentication.js +47 -0
- package/src/local/better-auth.js +517 -0
- package/src/local/cli.js +51 -0
- package/src/local/context.js +23 -0
- package/src/local/environment.js +109 -0
- package/src/local/index.js +204 -0
- package/src/local/router.js +999 -0
- package/src/local/server.js +664 -0
- package/src/local/store.js +530 -0
- package/src/local/test-environment.js +74 -0
- package/src/management-contracts.js +72 -0
- package/src/management.js +12 -0
- package/src/native-origin.js +75 -0
- package/src/postgres.js +494 -0
- package/src/react/core.js +743 -0
- package/src/react/index.js +99 -0
- package/src/result.js +251 -0
- package/src/schema.js +366 -0
- package/src/sql.js +996 -0
- package/src/svelte/index.js +129 -0
- package/src/tanstack/index.js +511 -0
- package/src/ts-auth-discovery.js +190 -0
- package/src/ts-auth.js +3497 -0
- package/types/admin.d.ts +6 -0
- package/types/better-auth.d.ts +8 -0
- package/types/config.d.ts +58 -0
- package/types/database-codec.d.ts +111 -0
- package/types/database-types.d.ts +213 -0
- package/types/database-view.d.ts +183 -0
- package/types/database.d.ts +98 -0
- package/types/discovery.d.ts +114 -0
- package/types/http.d.ts +46 -0
- package/types/index.d.ts +52 -0
- package/types/local/authentication.d.ts +11 -0
- package/types/local/better-auth.d.ts +33 -0
- package/types/local/cli.d.ts +2 -0
- package/types/local/context.d.ts +14 -0
- package/types/local/environment.d.ts +23 -0
- package/types/local/index.d.ts +94 -0
- package/types/local/router.d.ts +66 -0
- package/types/local/server.d.ts +54 -0
- package/types/local/store.d.ts +106 -0
- package/types/local/test-environment.d.ts +25 -0
- package/types/management-contracts.d.ts +44 -0
- package/types/management.d.ts +6 -0
- package/types/native-origin.d.ts +23 -0
- package/types/postgres.d.ts +123 -0
- package/types/react/core.d.ts +366 -0
- package/types/react/index.d.ts +54 -0
- package/types/result.d.ts +161 -0
- package/types/schema.d.ts +145 -0
- package/types/sql.d.ts +288 -0
- package/types/svelte/index.d.ts +81 -0
- package/types/tanstack/index.d.ts +165 -0
- package/types/ts-auth-discovery.d.ts +11 -0
- package/types/ts-auth.d.ts +1826 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { dirname, isAbsolute } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SCHEMA_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A small synchronous JSON record store for Node local adapters.
|
|
9
|
+
*
|
|
10
|
+
* This module intentionally imports node:sqlite and must not be used from a
|
|
11
|
+
* browser bundle. The database handle is kept private so consumers cannot
|
|
12
|
+
* bypass the store's serialization and transaction invariants.
|
|
13
|
+
*/
|
|
14
|
+
export class TerraScaleSqliteStore {
|
|
15
|
+
/**
|
|
16
|
+
* @readonly
|
|
17
|
+
* @type {string}
|
|
18
|
+
*/
|
|
19
|
+
databasePath;
|
|
20
|
+
|
|
21
|
+
/** @type {DatabaseSync} */
|
|
22
|
+
#database;
|
|
23
|
+
#closed = false;
|
|
24
|
+
/** @type {Promise<void>} */
|
|
25
|
+
#queue = Promise.resolve();
|
|
26
|
+
#savepointSequence = 0;
|
|
27
|
+
#transactionDepth = 0;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} databasePath
|
|
31
|
+
*/
|
|
32
|
+
constructor(databasePath) {
|
|
33
|
+
if (typeof databasePath !== "string") {
|
|
34
|
+
throw new TypeError("databasePath must be a string.");
|
|
35
|
+
}
|
|
36
|
+
if (databasePath !== ":memory:" && !isAbsolute(databasePath)) {
|
|
37
|
+
throw new TypeError(
|
|
38
|
+
"databasePath must be ':memory:' or an absolute path.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (databasePath !== ":memory:") {
|
|
43
|
+
mkdirSync(dirname(databasePath), { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.databasePath = databasePath;
|
|
47
|
+
this.#database = new DatabaseSync(databasePath);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// These are deliberately explicit PRAGMAs rather than relying only on
|
|
51
|
+
// constructor options, which vary between supported Node 22 releases.
|
|
52
|
+
this.#database.exec(
|
|
53
|
+
"PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;",
|
|
54
|
+
);
|
|
55
|
+
if (databasePath !== ":memory:") {
|
|
56
|
+
this.#database.exec("PRAGMA journal_mode = WAL;");
|
|
57
|
+
}
|
|
58
|
+
this.#migrate();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.#database.close();
|
|
61
|
+
this.#closed = true;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @returns {void} */
|
|
67
|
+
close() {
|
|
68
|
+
if (this.#closed) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (this.#transactionDepth !== 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Cannot close a SQLite store while a transaction is active.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
this.#database.close();
|
|
77
|
+
this.#closed = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @returns {void} */
|
|
81
|
+
reset() {
|
|
82
|
+
this.#assertOpen();
|
|
83
|
+
this.transaction(() => {
|
|
84
|
+
this.#database.exec("DELETE FROM records; DELETE FROM metadata;");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @template T
|
|
90
|
+
* @param {string} namespace
|
|
91
|
+
* @param {string} scope
|
|
92
|
+
* @param {string} id
|
|
93
|
+
* @returns {T | undefined}
|
|
94
|
+
*/
|
|
95
|
+
get(namespace, scope, id) {
|
|
96
|
+
this.#assertOpen();
|
|
97
|
+
assertKey(namespace, "namespace");
|
|
98
|
+
assertKey(scope, "scope");
|
|
99
|
+
assertKey(id, "id");
|
|
100
|
+
|
|
101
|
+
const row = this.#database
|
|
102
|
+
.prepare(
|
|
103
|
+
"SELECT value FROM records WHERE namespace = ? AND scope = ? AND id = ?",
|
|
104
|
+
)
|
|
105
|
+
.get(namespace, scope, id);
|
|
106
|
+
if (row === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return /** @type {T} */ (parseJson(row.value, "record"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @template T
|
|
114
|
+
* @param {string} namespace
|
|
115
|
+
* @param {string} scope
|
|
116
|
+
* @returns {T[]}
|
|
117
|
+
*/
|
|
118
|
+
list(namespace, scope) {
|
|
119
|
+
this.#assertOpen();
|
|
120
|
+
assertKey(namespace, "namespace");
|
|
121
|
+
assertKey(scope, "scope");
|
|
122
|
+
|
|
123
|
+
const rows = this.#database
|
|
124
|
+
.prepare(
|
|
125
|
+
"SELECT value FROM records WHERE namespace = ? AND scope = ? ORDER BY id ASC",
|
|
126
|
+
)
|
|
127
|
+
.all(namespace, scope);
|
|
128
|
+
return rows.map((row) => /** @type {T} */ (parseJson(row.value, "record")));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @template T
|
|
133
|
+
* @param {string} namespace
|
|
134
|
+
* @returns {readonly {
|
|
135
|
+
* readonly scope: string;
|
|
136
|
+
* readonly id: string;
|
|
137
|
+
* readonly value: T;
|
|
138
|
+
* }[]}
|
|
139
|
+
*/
|
|
140
|
+
listNamespace(namespace) {
|
|
141
|
+
this.#assertOpen();
|
|
142
|
+
assertKey(namespace, "namespace");
|
|
143
|
+
|
|
144
|
+
const rows = this.#database
|
|
145
|
+
.prepare(
|
|
146
|
+
"SELECT scope, id, value FROM records WHERE namespace = ? ORDER BY scope ASC, id ASC",
|
|
147
|
+
)
|
|
148
|
+
.all(namespace);
|
|
149
|
+
return rows.map((row) => ({
|
|
150
|
+
scope: String(row.scope),
|
|
151
|
+
id: String(row.id),
|
|
152
|
+
value: /** @type {T} */ (parseJson(row.value, "record")),
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @template T
|
|
158
|
+
* @param {string} namespace
|
|
159
|
+
* @param {string} scope
|
|
160
|
+
* @param {string} id
|
|
161
|
+
* @param {T} value
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
put(namespace, scope, id, value) {
|
|
165
|
+
this.#assertOpen();
|
|
166
|
+
assertKey(namespace, "namespace");
|
|
167
|
+
assertKey(scope, "scope");
|
|
168
|
+
assertKey(id, "id");
|
|
169
|
+
const encoded = stringifyJson(value);
|
|
170
|
+
|
|
171
|
+
this.#database
|
|
172
|
+
.prepare(
|
|
173
|
+
`INSERT INTO records (namespace, scope, id, value)
|
|
174
|
+
VALUES (?, ?, ?, ?)
|
|
175
|
+
ON CONFLICT(namespace, scope, id) DO UPDATE SET value = excluded.value`,
|
|
176
|
+
)
|
|
177
|
+
.run(namespace, scope, id, encoded);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} namespace
|
|
182
|
+
* @param {string} scope
|
|
183
|
+
* @param {string} id
|
|
184
|
+
* @returns {boolean}
|
|
185
|
+
*/
|
|
186
|
+
delete(namespace, scope, id) {
|
|
187
|
+
this.#assertOpen();
|
|
188
|
+
assertKey(namespace, "namespace");
|
|
189
|
+
assertKey(scope, "scope");
|
|
190
|
+
assertKey(id, "id");
|
|
191
|
+
|
|
192
|
+
const result = this.#database
|
|
193
|
+
.prepare(
|
|
194
|
+
"DELETE FROM records WHERE namespace = ? AND scope = ? AND id = ?",
|
|
195
|
+
)
|
|
196
|
+
.run(namespace, scope, id);
|
|
197
|
+
return Number(result.changes) !== 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} namespace
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
clearNamespace(namespace) {
|
|
205
|
+
this.#assertOpen();
|
|
206
|
+
assertKey(namespace, "namespace");
|
|
207
|
+
this.#database
|
|
208
|
+
.prepare("DELETE FROM records WHERE namespace = ?")
|
|
209
|
+
.run(namespace);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @template T
|
|
214
|
+
* @param {string} key
|
|
215
|
+
* @returns {T | undefined}
|
|
216
|
+
*/
|
|
217
|
+
getMeta(key) {
|
|
218
|
+
this.#assertOpen();
|
|
219
|
+
assertKey(key, "key");
|
|
220
|
+
|
|
221
|
+
const row = this.#database
|
|
222
|
+
.prepare("SELECT value FROM metadata WHERE key = ?")
|
|
223
|
+
.get(key);
|
|
224
|
+
if (row === undefined) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return /** @type {T} */ (parseJson(row.value, "metadata value"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @template T
|
|
232
|
+
* @param {string} key
|
|
233
|
+
* @param {T} value
|
|
234
|
+
* @returns {void}
|
|
235
|
+
*/
|
|
236
|
+
setMeta(key, value) {
|
|
237
|
+
this.#assertOpen();
|
|
238
|
+
assertKey(key, "key");
|
|
239
|
+
const encoded = stringifyJson(value);
|
|
240
|
+
|
|
241
|
+
this.#database
|
|
242
|
+
.prepare(
|
|
243
|
+
`INSERT INTO metadata (key, value)
|
|
244
|
+
VALUES (?, ?)
|
|
245
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
246
|
+
)
|
|
247
|
+
.run(key, encoded);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Serialize local native requests and async auth callbacks on this connection.
|
|
252
|
+
*
|
|
253
|
+
* @template T
|
|
254
|
+
* @param {() => Promise<T> | T} work
|
|
255
|
+
* @returns {Promise<T>}
|
|
256
|
+
*/
|
|
257
|
+
async exclusive(work) {
|
|
258
|
+
const prior = this.#queue;
|
|
259
|
+
/** @type {() => void} */
|
|
260
|
+
let release = () => {};
|
|
261
|
+
this.#queue = /** @type {Promise<void>} */ (
|
|
262
|
+
new Promise((resolve) => {
|
|
263
|
+
release = resolve;
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
await prior;
|
|
267
|
+
try {
|
|
268
|
+
this.#assertOpen();
|
|
269
|
+
return await work();
|
|
270
|
+
} finally {
|
|
271
|
+
release();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @template T
|
|
277
|
+
* @param {() => Promise<T>} work
|
|
278
|
+
* @returns {Promise<T>}
|
|
279
|
+
*/
|
|
280
|
+
async transactionAsync(work) {
|
|
281
|
+
return this.exclusive(async () => {
|
|
282
|
+
if (this.#transactionDepth !== 0)
|
|
283
|
+
throw new Error(
|
|
284
|
+
"Nested asynchronous SQLite transaction is unavailable.",
|
|
285
|
+
);
|
|
286
|
+
this.#database.exec("BEGIN IMMEDIATE;");
|
|
287
|
+
this.#transactionDepth += 1;
|
|
288
|
+
try {
|
|
289
|
+
const result = await work();
|
|
290
|
+
this.#database.exec("COMMIT;");
|
|
291
|
+
return result;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
this.#rollback();
|
|
294
|
+
throw error;
|
|
295
|
+
} finally {
|
|
296
|
+
this.#transactionDepth -= 1;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @template T
|
|
303
|
+
* @param {() => T} work
|
|
304
|
+
* @returns {T}
|
|
305
|
+
*/
|
|
306
|
+
transaction(work) {
|
|
307
|
+
this.#assertOpen();
|
|
308
|
+
if (typeof work !== "function") {
|
|
309
|
+
throw new TypeError("transaction work must be a function.");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const nested = this.#transactionDepth !== 0;
|
|
313
|
+
const savepoint = nested
|
|
314
|
+
? `terrascale_tx_${++this.#savepointSequence}`
|
|
315
|
+
: undefined;
|
|
316
|
+
if (savepoint === undefined) {
|
|
317
|
+
this.#database.exec("BEGIN IMMEDIATE;");
|
|
318
|
+
} else {
|
|
319
|
+
this.#database.exec(`SAVEPOINT ${savepoint};`);
|
|
320
|
+
}
|
|
321
|
+
this.#transactionDepth += 1;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const result = work();
|
|
325
|
+
if (result instanceof Promise)
|
|
326
|
+
throw new TypeError("Use transactionAsync for asynchronous callbacks.");
|
|
327
|
+
if (savepoint === undefined) {
|
|
328
|
+
this.#database.exec("COMMIT;");
|
|
329
|
+
} else {
|
|
330
|
+
this.#database.exec(`RELEASE SAVEPOINT ${savepoint};`);
|
|
331
|
+
}
|
|
332
|
+
this.#transactionDepth -= 1;
|
|
333
|
+
return result;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.#rollback(savepoint);
|
|
336
|
+
this.#transactionDepth -= 1;
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @returns {void} */
|
|
342
|
+
#migrate() {
|
|
343
|
+
this.#database.exec("BEGIN IMMEDIATE;");
|
|
344
|
+
try {
|
|
345
|
+
this.#database.exec(`
|
|
346
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
347
|
+
version INTEGER NOT NULL PRIMARY KEY,
|
|
348
|
+
applied_at TEXT NOT NULL
|
|
349
|
+
) WITHOUT ROWID;
|
|
350
|
+
`);
|
|
351
|
+
|
|
352
|
+
const row = this.#database
|
|
353
|
+
.prepare(
|
|
354
|
+
"SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1",
|
|
355
|
+
)
|
|
356
|
+
.get();
|
|
357
|
+
const version = row?.version;
|
|
358
|
+
if (version !== undefined && typeof version !== "number") {
|
|
359
|
+
throw new Error("Invalid schema migration version.");
|
|
360
|
+
}
|
|
361
|
+
if (version !== undefined && version > SCHEMA_VERSION) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Database schema version ${version} is newer than supported version ${SCHEMA_VERSION}.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (version === undefined) {
|
|
368
|
+
this.#database.exec(`
|
|
369
|
+
CREATE TABLE records (
|
|
370
|
+
namespace TEXT NOT NULL,
|
|
371
|
+
scope TEXT NOT NULL,
|
|
372
|
+
id TEXT NOT NULL,
|
|
373
|
+
value TEXT NOT NULL CHECK (json_valid(value) = 1),
|
|
374
|
+
PRIMARY KEY (namespace, scope, id)
|
|
375
|
+
) WITHOUT ROWID;
|
|
376
|
+
|
|
377
|
+
CREATE TABLE metadata (
|
|
378
|
+
key TEXT NOT NULL PRIMARY KEY,
|
|
379
|
+
value TEXT NOT NULL CHECK (json_valid(value) = 1)
|
|
380
|
+
) WITHOUT ROWID;
|
|
381
|
+
`);
|
|
382
|
+
this.#database
|
|
383
|
+
.prepare(
|
|
384
|
+
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)",
|
|
385
|
+
)
|
|
386
|
+
.run(SCHEMA_VERSION, new Date().toISOString());
|
|
387
|
+
}
|
|
388
|
+
this.#database.exec("COMMIT;");
|
|
389
|
+
} catch (error) {
|
|
390
|
+
this.#rollback();
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @param {string | undefined} [savepoint]
|
|
397
|
+
* @returns {void}
|
|
398
|
+
*/
|
|
399
|
+
#rollback(savepoint = undefined) {
|
|
400
|
+
try {
|
|
401
|
+
if (savepoint === undefined) {
|
|
402
|
+
this.#database.exec("ROLLBACK;");
|
|
403
|
+
} else {
|
|
404
|
+
this.#database.exec(
|
|
405
|
+
`ROLLBACK TO SAVEPOINT ${savepoint}; RELEASE SAVEPOINT ${savepoint};`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
// Preserve the original work/migration exception. SQLite will normally
|
|
410
|
+
// have already rolled back a failed transaction automatically.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** @returns {void} */
|
|
415
|
+
#assertOpen() {
|
|
416
|
+
if (this.#closed) {
|
|
417
|
+
throw new Error("The SQLite store is closed.");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @param {string} value
|
|
424
|
+
* @param {string} name
|
|
425
|
+
* @returns {void}
|
|
426
|
+
*/
|
|
427
|
+
function assertKey(value, name) {
|
|
428
|
+
if (typeof value !== "string") {
|
|
429
|
+
throw new TypeError(`${name} must be a string.`);
|
|
430
|
+
}
|
|
431
|
+
if (value.includes("\u0000")) {
|
|
432
|
+
throw new TypeError(`${name} must not contain a NUL character.`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* @param {unknown} value
|
|
438
|
+
* @returns {string}
|
|
439
|
+
*/
|
|
440
|
+
function stringifyJson(value) {
|
|
441
|
+
try {
|
|
442
|
+
assertJsonValue(value, new WeakSet());
|
|
443
|
+
const encoded = JSON.stringify(value);
|
|
444
|
+
if (encoded === undefined) {
|
|
445
|
+
throw new TypeError("Value must be JSON serializable.");
|
|
446
|
+
}
|
|
447
|
+
// JSON.stringify is expected to produce valid JSON, but parsing here
|
|
448
|
+
// keeps the invariant explicit before handing data to SQLite.
|
|
449
|
+
JSON.parse(encoded);
|
|
450
|
+
return encoded;
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (
|
|
453
|
+
error instanceof TypeError &&
|
|
454
|
+
error.message === "Value must be JSON serializable."
|
|
455
|
+
) {
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
throw new TypeError("Value must be JSON serializable.", { cause: error });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {unknown} value
|
|
464
|
+
* @param {WeakSet<object>} ancestors
|
|
465
|
+
* @returns {void}
|
|
466
|
+
*/
|
|
467
|
+
function assertJsonValue(value, ancestors) {
|
|
468
|
+
if (
|
|
469
|
+
value === null ||
|
|
470
|
+
typeof value === "string" ||
|
|
471
|
+
typeof value === "boolean"
|
|
472
|
+
) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (typeof value === "number") {
|
|
476
|
+
if (!Number.isFinite(value)) {
|
|
477
|
+
throw new TypeError("JSON numbers must be finite.");
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (
|
|
482
|
+
typeof value === "undefined" ||
|
|
483
|
+
typeof value === "bigint" ||
|
|
484
|
+
typeof value === "function" ||
|
|
485
|
+
typeof value === "symbol"
|
|
486
|
+
) {
|
|
487
|
+
throw new TypeError("Value contains a non-JSON value.");
|
|
488
|
+
}
|
|
489
|
+
if (typeof value !== "object") {
|
|
490
|
+
throw new TypeError("Value contains a non-JSON value.");
|
|
491
|
+
}
|
|
492
|
+
if (ancestors.has(value)) {
|
|
493
|
+
throw new TypeError("Value contains a circular reference.");
|
|
494
|
+
}
|
|
495
|
+
ancestors.add(value);
|
|
496
|
+
if (Array.isArray(value)) {
|
|
497
|
+
for (const item of value) {
|
|
498
|
+
assertJsonValue(item, ancestors);
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
const objectValue = /** @type {Record<string, unknown>} */ (value);
|
|
502
|
+
for (const key of Object.keys(value)) {
|
|
503
|
+
assertJsonValue(objectValue[key], ancestors);
|
|
504
|
+
}
|
|
505
|
+
for (const symbol of Object.getOwnPropertySymbols(value)) {
|
|
506
|
+
if (Object.prototype.propertyIsEnumerable.call(value, symbol)) {
|
|
507
|
+
throw new TypeError("Value contains a symbol property.");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
ancestors.delete(value);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @param {unknown} encoded
|
|
516
|
+
* @param {string} label
|
|
517
|
+
* @returns {unknown}
|
|
518
|
+
*/
|
|
519
|
+
function parseJson(encoded, label) {
|
|
520
|
+
if (typeof encoded !== "string") {
|
|
521
|
+
throw new Error(`Invalid ${label}: SQLite value is not text.`);
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
return JSON.parse(encoded);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw new Error(`Invalid ${label}: stored JSON is malformed.`, {
|
|
527
|
+
cause: error,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { assertLocalSqliteEnvironment } from "./environment.js";
|
|
2
|
+
/** Local development tests never relax CI/deployment/qualification guards. */
|
|
3
|
+
export const localSqliteUnitTestsAllowed = (() => {
|
|
4
|
+
try {
|
|
5
|
+
assertLocalSqliteEnvironment();
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
})();
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
15
|
+
import { createTerraScaleSqliteRuntime } from "./index.js";
|
|
16
|
+
/** @import { TerraScaleSqliteRuntimeOptions } from "./index.js" */
|
|
17
|
+
/** @import { DatabaseCommit, DatabaseField, DatabasePoint } from "../database-types.js" */
|
|
18
|
+
/**
|
|
19
|
+
* @param {Omit<TerraScaleSqliteRuntimeOptions, "sqlite">} [options]
|
|
20
|
+
*/
|
|
21
|
+
export async function localFixture(options = {}) {
|
|
22
|
+
const directory = await mkdtemp(join(tmpdir(), "terrascale-local-native-"));
|
|
23
|
+
const databasePath = join(directory, "development.sqlite");
|
|
24
|
+
const runtime = createTerraScaleSqliteRuntime({
|
|
25
|
+
...options,
|
|
26
|
+
sqlite: { enabled: true, databasePath },
|
|
27
|
+
});
|
|
28
|
+
const collection = randomUUID();
|
|
29
|
+
const binding = {
|
|
30
|
+
collection,
|
|
31
|
+
schema_generation: 1n,
|
|
32
|
+
schema_digest: "a".repeat(64),
|
|
33
|
+
};
|
|
34
|
+
await runtime.bindCollection(binding);
|
|
35
|
+
const client = runtime.createClient();
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} document
|
|
38
|
+
* @param {readonly DatabaseField[]} fields
|
|
39
|
+
* @param {DatabasePoint} [point]
|
|
40
|
+
* @returns {DatabaseCommit}
|
|
41
|
+
*/
|
|
42
|
+
const commit = (document, fields, point) => ({
|
|
43
|
+
transaction_id: {
|
|
44
|
+
expires_at_unix_ms: BigInt(Date.now() + 60_000),
|
|
45
|
+
nonce: randomBytes(16).toString("hex"),
|
|
46
|
+
},
|
|
47
|
+
authority: runtime.authority,
|
|
48
|
+
writes: [
|
|
49
|
+
{
|
|
50
|
+
...binding,
|
|
51
|
+
document,
|
|
52
|
+
condition:
|
|
53
|
+
point === undefined
|
|
54
|
+
? { kind: "absent" }
|
|
55
|
+
: { kind: "version", version: /** @type {string} */ (point.state_version) },
|
|
56
|
+
observed: point?.context ?? [],
|
|
57
|
+
value: { kind: "live", fields },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
runtime,
|
|
63
|
+
client,
|
|
64
|
+
collection,
|
|
65
|
+
binding,
|
|
66
|
+
directory,
|
|
67
|
+
databasePath,
|
|
68
|
+
commit,
|
|
69
|
+
close: async () => {
|
|
70
|
+
await runtime.close();
|
|
71
|
+
await rm(directory, { recursive: true, force: true });
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Required capabilities whose producer/runtime admission is still open. */
|
|
2
|
+
export const terraScaleReleaseBlockers = Object.freeze({
|
|
3
|
+
management: Object.freeze({
|
|
4
|
+
code: "management_contract_unavailable",
|
|
5
|
+
owner: "C06/C07 regional management service",
|
|
6
|
+
required: Object.freeze([
|
|
7
|
+
"resource provisioning",
|
|
8
|
+
"schema/catalog plan/apply/status",
|
|
9
|
+
"credential lifecycle",
|
|
10
|
+
"backups/restore",
|
|
11
|
+
"imports/exports",
|
|
12
|
+
"usage",
|
|
13
|
+
"administration",
|
|
14
|
+
]),
|
|
15
|
+
}),
|
|
16
|
+
betterAuth: Object.freeze({
|
|
17
|
+
code: "security_authority_unavailable",
|
|
18
|
+
owner: "native single-writer security admission and C06/C07",
|
|
19
|
+
required: Object.freeze([
|
|
20
|
+
"unique insert",
|
|
21
|
+
"compare-and-swap",
|
|
22
|
+
"one-time consume",
|
|
23
|
+
"bounded transactions",
|
|
24
|
+
"pagination/joins",
|
|
25
|
+
"indexes/migrations",
|
|
26
|
+
"revocation/denial",
|
|
27
|
+
"region concurrency",
|
|
28
|
+
]),
|
|
29
|
+
}),
|
|
30
|
+
qualification: Object.freeze({
|
|
31
|
+
code: "runtime_qualification_unavailable",
|
|
32
|
+
owner: "real producer allocation and deployed customer application",
|
|
33
|
+
required: Object.freeze([
|
|
34
|
+
"installed package against real producer",
|
|
35
|
+
"deployed authenticated account/browser journey",
|
|
36
|
+
]),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
/** @typedef {keyof typeof terraScaleReleaseBlockers} TerraScaleBlockedCapability */
|
|
40
|
+
export class TerraScaleCapabilityUnavailableError extends Error {
|
|
41
|
+
/**
|
|
42
|
+
* @readonly
|
|
43
|
+
* @type {string}
|
|
44
|
+
*/
|
|
45
|
+
code;
|
|
46
|
+
/**
|
|
47
|
+
* @readonly
|
|
48
|
+
* @type {TerraScaleBlockedCapability}
|
|
49
|
+
*/
|
|
50
|
+
capability;
|
|
51
|
+
/**
|
|
52
|
+
* @readonly
|
|
53
|
+
* @type {readonly string[]}
|
|
54
|
+
*/
|
|
55
|
+
required;
|
|
56
|
+
/** @param {TerraScaleBlockedCapability} capability */
|
|
57
|
+
constructor(capability) {
|
|
58
|
+
const status = terraScaleReleaseBlockers[capability];
|
|
59
|
+
super(`${capability} requires admitted ${status.owner}.`);
|
|
60
|
+
this.name = "TerraScaleCapabilityUnavailableError";
|
|
61
|
+
this.code = status.code;
|
|
62
|
+
this.capability = capability;
|
|
63
|
+
this.required = status.required;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @param {TerraScaleBlockedCapability} capability
|
|
68
|
+
* @returns {never}
|
|
69
|
+
*/
|
|
70
|
+
export function requireTerraScaleCapability(capability) {
|
|
71
|
+
throw new TerraScaleCapabilityUnavailableError(capability);
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
terraScaleReleaseBlockers,
|
|
3
|
+
TerraScaleCapabilityUnavailableError,
|
|
4
|
+
} from "./management-contracts.js";
|
|
5
|
+
import { requireTerraScaleCapability } from "./management-contracts.js";
|
|
6
|
+
/**
|
|
7
|
+
* Required public management support remains closed until C06/C07 route admission.
|
|
8
|
+
* @returns {never}
|
|
9
|
+
*/
|
|
10
|
+
export function requireTerraScaleManagement() {
|
|
11
|
+
return requireTerraScaleCapability("management");
|
|
12
|
+
}
|