@terrascale/sdk 0.0.1 → 0.0.3
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/CHANGELOG.md +35 -0
- package/README.md +46 -17
- package/package.json +5 -2
- package/sdk-v1-route-manifest.json +7593 -1175
- package/src/database-codec.js +21 -26
- package/src/database-types.js +11 -11
- package/src/database-view.js +6 -6
- package/src/http.js +26 -5
- package/src/ids.js +22 -0
- package/src/index.js +2 -1
- package/src/local/better-auth.js +17 -6
- package/src/local/index.js +2 -2
- package/src/local/legacy-client.js +5 -5
- package/src/local/router.js +12 -12
- package/src/local/store.js +29 -28
- package/src/local/test-environment.js +3 -2
- package/src/postgres.js +5 -4
- package/src/react/core.js +2 -2
- package/src/ts-auth-discovery.js +17 -0
- package/src/ts-auth-ids.js +122 -0
- package/src/ts-auth.js +75 -72
- package/src/v1/decode.js +30 -10
- package/src/v1/ids.js +10 -62
- package/src/v1/routes.js +5 -5
- package/src/v1/shapes.js +6 -7
- package/src/v1/terrabase-v1.js +24 -16
- package/src/v1/transport.js +6 -4
- package/types/database-codec.d.ts +1 -6
- package/types/database-types.d.ts +11 -11
- package/types/http.d.ts +1 -0
- package/types/ids.d.ts +10 -0
- package/types/index.d.ts +2 -1
- package/types/local/store.d.ts +10 -0
- package/types/local/test-environment.d.ts +2 -2
- package/types/postgres.d.ts +4 -4
- package/types/ts-auth-discovery.d.ts +8 -0
- package/types/ts-auth-ids.d.ts +14 -0
- package/types/ts-auth.d.ts +99 -99
- package/types/v1/decode.d.ts +27 -11
- package/types/v1/ids.d.ts +5 -17
- package/types/v1/shapes.d.ts +2 -2
- package/types/v1/terrabase-v1.d.ts +3 -3
- package/types/v1/transport.d.ts +2 -2
package/src/database-codec.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireId as requireDatabaseId } from "./ids.js";
|
|
2
|
+
export { requireId as requireDatabaseId } from "./ids.js";
|
|
1
3
|
/**
|
|
2
4
|
* @import {
|
|
3
5
|
* DatabaseAuthority, DatabaseCommit, DatabaseCommitReceipt, DatabaseCondition, DatabaseEvent,
|
|
@@ -20,18 +22,6 @@ const maximumBodyBytes = 4 * 1024 * 1024;
|
|
|
20
22
|
const maximumValueBytes = 1024 * 1024;
|
|
21
23
|
const scalarTypes = new Set(["bool", "i64", "utf8", "bytes"]);
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
* @param {unknown} value
|
|
25
|
-
* @param {string} [name]
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export function requireDatabaseUuid(value, name = "UUID") {
|
|
29
|
-
if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(value)) {
|
|
30
|
-
throw new TypeError(`${name} must be a canonical lowercase UUID.`);
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
25
|
/**
|
|
36
26
|
* @param {unknown} value
|
|
37
27
|
* @param {string} [name]
|
|
@@ -295,7 +285,7 @@ function observedToWire(entries) {
|
|
|
295
285
|
let previous = "";
|
|
296
286
|
return array(entries).map(raw => {
|
|
297
287
|
const entry = object(raw, ["replica", "incarnation", "sequence"]);
|
|
298
|
-
const replica =
|
|
288
|
+
const replica = requireDatabaseId(entry.replica, "replica");
|
|
299
289
|
const incarnation = hex(entry.incarnation, 16, "incarnation");
|
|
300
290
|
const order = replica + incarnation;
|
|
301
291
|
if (order <= previous) throw new TypeError("Observed origins must be strictly increasing.");
|
|
@@ -311,7 +301,7 @@ function observedToWire(entries) {
|
|
|
311
301
|
function observedFromWire(raw) {
|
|
312
302
|
const entries = array(raw).map(value => {
|
|
313
303
|
const entry = object(value, ["replica", "incarnation", "sequence"]);
|
|
314
|
-
return { replica:
|
|
304
|
+
return { replica: requireDatabaseId(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"), sequence: decimal(entry.sequence, "sequence", true) };
|
|
315
305
|
});
|
|
316
306
|
observedToWire(entries);
|
|
317
307
|
return entries;
|
|
@@ -324,8 +314,8 @@ function observedFromWire(raw) {
|
|
|
324
314
|
function eventFromWire(raw) {
|
|
325
315
|
const entry = object(raw, ["database", "shard", "replica", "incarnation", "sequence"]);
|
|
326
316
|
return {
|
|
327
|
-
database:
|
|
328
|
-
replica:
|
|
317
|
+
database: requireDatabaseId(entry.database, "database"), shard: decimal(entry.shard, "shard"),
|
|
318
|
+
replica: requireDatabaseId(entry.replica, "replica"), incarnation: hex(entry.incarnation, 16, "incarnation"),
|
|
329
319
|
sequence: decimal(entry.sequence, "sequence", true),
|
|
330
320
|
};
|
|
331
321
|
}
|
|
@@ -430,7 +420,7 @@ function rowsFromWire(raw) {
|
|
|
430
420
|
let previous = "";
|
|
431
421
|
return array(raw, 128).map(rawRow => {
|
|
432
422
|
const entry = object(rawRow, ["document", "point"]);
|
|
433
|
-
const document =
|
|
423
|
+
const document = requireDatabaseId(entry.document, "document");
|
|
434
424
|
if (document <= previous) throw new TypeError("Query documents must be strictly increasing.");
|
|
435
425
|
previous = document;
|
|
436
426
|
return { document, point: decodeDatabasePoint(entry.point) };
|
|
@@ -486,7 +476,7 @@ export function decodeDatabaseSubscription(raw) {
|
|
|
486
476
|
previous = sequence;
|
|
487
477
|
return { sequence, effects: array(group.effects, 128).map(rawEffect => {
|
|
488
478
|
const effect = object(rawEffect, ["document", "point"]);
|
|
489
|
-
return { document:
|
|
479
|
+
return { document: requireDatabaseId(effect.document, "document"), point: effect.point === null ? null : decodeDatabasePoint(effect.point) };
|
|
490
480
|
}) };
|
|
491
481
|
});
|
|
492
482
|
return { version: 1, phase: "poll", groups, poll_cursor: cursor(entry.poll_cursor) };
|
|
@@ -501,7 +491,7 @@ export function encodeDatabaseAuthority(authority) {
|
|
|
501
491
|
object(authority, ["mode", "generation", "writer"]);
|
|
502
492
|
if (authority.mode !== "single_writer") throw new TypeError("Unsupported authority mode.");
|
|
503
493
|
const writer = object(authority.writer, ["replica", "incarnation"]);
|
|
504
|
-
return { mode: "single_writer", generation: requireDatabaseU64(authority.generation, "authority generation", true).toString(), writer: { replica:
|
|
494
|
+
return { mode: "single_writer", generation: requireDatabaseU64(authority.generation, "authority generation", true).toString(), writer: { replica: requireDatabaseId(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
|
|
505
495
|
}
|
|
506
496
|
|
|
507
497
|
/**
|
|
@@ -514,7 +504,7 @@ export function decodeDatabaseAuthority(raw) {
|
|
|
514
504
|
const entry = object(raw, ["mode", "generation", "writer"]);
|
|
515
505
|
if (entry.mode !== "single_writer") throw new TypeError("Unsupported authority mode.");
|
|
516
506
|
const writer = object(entry.writer, ["replica", "incarnation"]);
|
|
517
|
-
return { mode: "single_writer", generation: decimal(entry.generation, "authority generation", true), writer: { replica:
|
|
507
|
+
return { mode: "single_writer", generation: decimal(entry.generation, "authority generation", true), writer: { replica: requireDatabaseId(writer.replica, "writer"), incarnation: hex(writer.incarnation, 16, "incarnation") } };
|
|
518
508
|
}
|
|
519
509
|
|
|
520
510
|
/**
|
|
@@ -569,7 +559,7 @@ function writeToWire(write, authority) {
|
|
|
569
559
|
: (object(write.value, ["kind", "fields"]), { kind: "live", fields: fieldsToWire(write.value.fields, 1, { nodes: 0 }) });
|
|
570
560
|
if (write.value.kind !== "live" && write.value.kind !== "tombstone") throw new TypeError("Unsupported write kind.");
|
|
571
561
|
if (value.kind === "live") validateNativeDocument(/** @type {readonly Json[]} */ (value.fields));
|
|
572
|
-
return { collection:
|
|
562
|
+
return { collection: requireDatabaseId(write.collection, "collection"), document: requireDatabaseId(write.document, "document"), schema_generation: requireDatabaseU64(write.schema_generation, "schema generation", true).toString(), schema_digest: hex(write.schema_digest, 32, "schema digest"), condition: conditionToWire(write.condition), observed: observedToWire(write.observed), value };
|
|
573
563
|
}
|
|
574
564
|
|
|
575
565
|
class Bytes {
|
|
@@ -603,7 +593,12 @@ class Bytes {
|
|
|
603
593
|
* @param {string} value
|
|
604
594
|
* @returns {void}
|
|
605
595
|
*/
|
|
606
|
-
|
|
596
|
+
id(value) {
|
|
597
|
+
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
598
|
+
let bits = 0n;
|
|
599
|
+
for (const char of requireDatabaseId(value)) bits = (bits << 5n) | BigInt(alphabet.indexOf(char));
|
|
600
|
+
this.hex(bits.toString(16).padStart(32, "0"));
|
|
601
|
+
}
|
|
607
602
|
/** @returns {Uint8Array} */
|
|
608
603
|
finish() { const bytes = new Uint8Array(this.length); let offset = 0; for (const chunk of this.chunks) { bytes.set(chunk, offset); offset += chunk.length; } return bytes; }
|
|
609
604
|
}
|
|
@@ -667,22 +662,22 @@ function validateNativeDocument(fields) {
|
|
|
667
662
|
function effectBytes(scope, authority, writes) {
|
|
668
663
|
const bytes = new Bytes();
|
|
669
664
|
bytes.put(encoder.encode("TB-DOC-EFFECT-v1\0"));
|
|
670
|
-
bytes.
|
|
665
|
+
bytes.id(requireDatabaseId(scope.tenant, "tenant")); bytes.id(requireDatabaseId(scope.database, "database")); bytes.integer(requireDatabaseU64(scope.shard, "shard"), 8);
|
|
671
666
|
const singleWriter = authority.mode === "single_writer";
|
|
672
667
|
bytes.integer(singleWriter ? 1n : 0n, 1);
|
|
673
668
|
if (singleWriter) {
|
|
674
669
|
const writer = object(authority.writer, ["replica", "incarnation"]);
|
|
675
|
-
bytes.integer(BigInt(/** @type {string} */ (authority.generation)), 8); bytes.
|
|
670
|
+
bytes.integer(BigInt(/** @type {string} */ (authority.generation)), 8); bytes.id(/** @type {string} */ (writer.replica)); bytes.hex(/** @type {string} */ (writer.incarnation));
|
|
676
671
|
}
|
|
677
672
|
bytes.integer(BigInt(writes.length), 8);
|
|
678
673
|
for (const write of writes) {
|
|
679
|
-
bytes.
|
|
674
|
+
bytes.id(/** @type {string} */ (write.collection)); bytes.id(/** @type {string} */ (write.document)); bytes.integer(BigInt(/** @type {string} */ (write.schema_generation)), 8); bytes.hex(/** @type {string} */ (write.schema_digest));
|
|
680
675
|
const condition = object(write.condition, ["kind"], ["version"]);
|
|
681
676
|
bytes.integer(condition.kind === "none" ? 0n : condition.kind === "absent" ? 1n : 2n, 1);
|
|
682
677
|
if (condition.kind === "version") bytes.hex(/** @type {string} */ (condition.version));
|
|
683
678
|
const observed = array(write.observed);
|
|
684
679
|
bytes.integer(BigInt(observed.length), 8);
|
|
685
|
-
for (const raw of observed) { const entry = object(raw, ["replica", "incarnation", "sequence"]); bytes.
|
|
680
|
+
for (const raw of observed) { const entry = object(raw, ["replica", "incarnation", "sequence"]); bytes.id(/** @type {string} */ (entry.replica)); bytes.hex(/** @type {string} */ (entry.incarnation)); bytes.integer(BigInt(/** @type {string} */ (entry.sequence)), 8); }
|
|
686
681
|
const value = object(write.value, ["kind"], ["fields"]);
|
|
687
682
|
bytes.integer(value.kind === "tombstone" ? 0n : 1n, 1);
|
|
688
683
|
if (value.kind === "live") {
|
package/src/database-types.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @typedef {
|
|
2
|
+
* Entity identities are canonical uppercase ULIDs, including explicit local development.
|
|
3
|
+
* @typedef {import("./ids.js").EntityId} EntityId
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* All 64-bit quantities stay bigint in JavaScript and decimal strings on the wire.
|
|
8
8
|
* @typedef {{
|
|
9
|
-
* readonly tenant:
|
|
10
|
-
* readonly database:
|
|
9
|
+
* readonly tenant: EntityId;
|
|
10
|
+
* readonly database: EntityId;
|
|
11
11
|
* readonly shard: bigint;
|
|
12
12
|
* }} DatabaseScope
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {{
|
|
17
|
-
* readonly collection:
|
|
17
|
+
* readonly collection: EntityId;
|
|
18
18
|
* readonly schema_generation: bigint;
|
|
19
19
|
* readonly schema_digest: string;
|
|
20
20
|
* }} CollectionBinding
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* @typedef {{
|
|
42
|
-
* readonly replica:
|
|
42
|
+
* readonly replica: EntityId;
|
|
43
43
|
* readonly incarnation: string;
|
|
44
44
|
* readonly sequence: bigint;
|
|
45
45
|
* }} DatabaseObserved
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* @typedef {DatabaseObserved & {
|
|
50
|
-
* readonly database:
|
|
50
|
+
* readonly database: EntityId;
|
|
51
51
|
* readonly shard: bigint;
|
|
52
52
|
* }} DatabaseEvent
|
|
53
53
|
*/
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
* | {
|
|
59
59
|
* readonly mode: "single_writer";
|
|
60
60
|
* readonly generation: bigint;
|
|
61
|
-
* readonly writer: { readonly replica:
|
|
61
|
+
* readonly writer: { readonly replica: EntityId; readonly incarnation: string };
|
|
62
62
|
* }
|
|
63
63
|
* )} DatabaseAuthority
|
|
64
64
|
*/
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* @typedef {CollectionBinding & {
|
|
83
|
-
* readonly document:
|
|
83
|
+
* readonly document: EntityId;
|
|
84
84
|
* readonly condition: DatabaseCondition;
|
|
85
85
|
* readonly observed: readonly DatabaseObserved[];
|
|
86
86
|
* readonly value: DatabaseWriteValue;
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
* }} DatabaseQuery
|
|
136
136
|
*/
|
|
137
137
|
|
|
138
|
-
/** @typedef {{ readonly document:
|
|
138
|
+
/** @typedef {{ readonly document: EntityId; readonly point: DatabasePoint }} DatabaseQueryRow */
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* @typedef {{
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
* )} DatabaseSubscriptionRequest
|
|
203
203
|
*/
|
|
204
204
|
|
|
205
|
-
/** @typedef {{ readonly document:
|
|
205
|
+
/** @typedef {{ readonly document: EntityId; readonly point: DatabasePoint | null }} DatabaseSubscriptionEffect */
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* @typedef {{
|
package/src/database-view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { encodeDatabaseSubscription,
|
|
1
|
+
import { encodeDatabaseSubscription, requireDatabaseId, requireDatabaseU64 } from "./database-codec.js";
|
|
2
2
|
/**
|
|
3
3
|
* @import { TerraBaseClient } from "./local/legacy-client.js"
|
|
4
4
|
* @import {
|
|
@@ -213,14 +213,14 @@ export class DatabaseSubscriptionView {
|
|
|
213
213
|
*/
|
|
214
214
|
constructor(scope, options) {
|
|
215
215
|
const capturedScope = Object.freeze({
|
|
216
|
-
tenant:
|
|
217
|
-
database:
|
|
216
|
+
tenant: requireDatabaseId(scope.tenant, "tenant"),
|
|
217
|
+
database: requireDatabaseId(scope.database, "database"),
|
|
218
218
|
shard: requireDatabaseU64(scope.shard, "shard"),
|
|
219
219
|
});
|
|
220
220
|
const predicate = predicateCopy(options.predicate);
|
|
221
221
|
const limit = boundedInteger(options.limit, 128, 128, "Snapshot page size");
|
|
222
222
|
encodeDatabaseSubscription({ phase: "snapshot", predicate, limit });
|
|
223
|
-
this.binding = Object.freeze({ scope: capturedScope, collection:
|
|
223
|
+
this.binding = Object.freeze({ scope: capturedScope, collection: requireDatabaseId(options.collection, "collection"), predicate, limit });
|
|
224
224
|
this.#maximumRows = boundedInteger(options.maximumRows, 10_000, 1_000_000, "Maximum rows");
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -258,7 +258,7 @@ export class DatabaseSubscriptionView {
|
|
|
258
258
|
if (response.rows.length > this.binding.limit) throw new DatabaseSubscriptionError("page_limit_exceeded");
|
|
259
259
|
let previous = this.#snapshotRows.at(-1)?.document ?? "";
|
|
260
260
|
const page = response.rows.map(row => {
|
|
261
|
-
const document =
|
|
261
|
+
const document = requireDatabaseId(row.document, "document");
|
|
262
262
|
if (document <= previous) throw new DatabaseSubscriptionError("snapshot_order_invalid");
|
|
263
263
|
previous = document;
|
|
264
264
|
return Object.freeze({ document, point: pointCopy(row.point, this.binding.scope) });
|
|
@@ -293,7 +293,7 @@ export class DatabaseSubscriptionView {
|
|
|
293
293
|
/** @type {Set<string>} */
|
|
294
294
|
const documents = new Set();
|
|
295
295
|
for (const effect of group.effects) {
|
|
296
|
-
const document =
|
|
296
|
+
const document = requireDatabaseId(effect.document, "document");
|
|
297
297
|
if (documents.has(document)) throw new DatabaseSubscriptionError("duplicate_group_document");
|
|
298
298
|
documents.add(document);
|
|
299
299
|
if (effect.point === null) rows.delete(document);
|
package/src/http.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { resolveAccessToken, resolveTsAuthRuntimeConfig } from "./config.js";
|
|
2
2
|
import { err, ok, sanitizeDiagnostic } from "./result.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
regionalIssuerOrigin,
|
|
5
|
+
TsAuthEndpointDiscovery,
|
|
6
|
+
} from "./ts-auth-discovery.js";
|
|
4
7
|
import { parseDatabaseJson } from "./database-codec.js";
|
|
8
|
+
import { validateTsAuthIds } from "./ts-auth-ids.js";
|
|
5
9
|
/** @import { ResolvedTsAuthRuntimeConfig, TsAuthRuntimeConfig, FetchLike } from "./config.js" */
|
|
6
10
|
/** @import { TerraScaleError, TerraScaleErrorCategory, TerraScaleResult, TerraScaleValidationIssue } from "./result.js" */
|
|
7
11
|
|
|
@@ -18,6 +22,7 @@ import { parseDatabaseJson } from "./database-codec.js";
|
|
|
18
22
|
* readonly formBody?: URLSearchParams | string | undefined;
|
|
19
23
|
* readonly userinfoAccessToken?: string | undefined;
|
|
20
24
|
* readonly resourceIntrospectionBasic?: string | undefined;
|
|
25
|
+
* readonly issuer?: string | undefined; // Behind the Atlas proxy a regional issuer is called on its own host.
|
|
21
26
|
* readonly signal?: AbortSignal | undefined;
|
|
22
27
|
* }} TsAuthRequest
|
|
23
28
|
*/
|
|
@@ -85,10 +90,17 @@ export class TsAuthHttp {
|
|
|
85
90
|
options.body !== undefined
|
|
86
91
|
? JSON.stringify(options.body)
|
|
87
92
|
: options.formBody?.toString();
|
|
88
|
-
const
|
|
89
|
-
this.#discovery
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const issuerOrigin =
|
|
94
|
+
this.#discovery === undefined || options.issuer === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: regionalIssuerOrigin(options.issuer);
|
|
97
|
+
const endpoints =
|
|
98
|
+
issuerOrigin === undefined
|
|
99
|
+
? await withCancellation(
|
|
100
|
+
this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
|
|
101
|
+
signal,
|
|
102
|
+
)
|
|
103
|
+
: [issuerOrigin];
|
|
92
104
|
let endpointIndex = 0;
|
|
93
105
|
let attempt = 0;
|
|
94
106
|
let refreshed = false;
|
|
@@ -216,6 +228,15 @@ export class TsAuthHttp {
|
|
|
216
228
|
response.status,
|
|
217
229
|
response.headers,
|
|
218
230
|
);
|
|
231
|
+
try {
|
|
232
|
+
validateTsAuthIds(parsed.value, options.routeName);
|
|
233
|
+
} catch {
|
|
234
|
+
return err(options.routeName, {
|
|
235
|
+
kind: "invalid-response", code: "invalid_response",
|
|
236
|
+
message: "ts-auth returned a noncanonical entity ID.", status: response.status,
|
|
237
|
+
requestId: requestIdFrom(response.headers),
|
|
238
|
+
}, response.status, response.headers);
|
|
239
|
+
}
|
|
219
240
|
return ok(
|
|
220
241
|
options.routeName,
|
|
221
242
|
response.status,
|
package/src/ids.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isValid, monotonicFactory } from "ulid";
|
|
2
|
+
|
|
3
|
+
/** A plain 26-character, uppercase Crockford ULID. @typedef {string} EntityId */
|
|
4
|
+
|
|
5
|
+
const nextId = monotonicFactory();
|
|
6
|
+
|
|
7
|
+
/** @returns {EntityId} Monotonic within a millisecond, including clock rollback. */
|
|
8
|
+
export function generateId() {
|
|
9
|
+
return nextId();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** @param {unknown} value @returns {value is EntityId} */
|
|
13
|
+
export function isValidId(value) {
|
|
14
|
+
// The library also accepts lowercase and 130-bit overflow; the public contract does not.
|
|
15
|
+
return typeof value === "string" && isValid(value) && value === value.toUpperCase() && /^[0-7]/u.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {unknown} value @param {string} [name] @returns {EntityId} */
|
|
19
|
+
export function requireId(value, name = "Entity ID") {
|
|
20
|
+
if (!isValidId(value)) throw new TypeError(`${name} must be a plain 26-character canonical uppercase Crockford ULID.`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,8 @@ export {
|
|
|
19
19
|
isTerraBaseV1Error,
|
|
20
20
|
} from "./v1/errors.js";
|
|
21
21
|
export { v1Filter } from "./v1/filter.js";
|
|
22
|
-
export {
|
|
22
|
+
export { generateId, isValidId, requireId } from "./ids.js";
|
|
23
|
+
/** @typedef {import("./ids.js").EntityId} EntityId */
|
|
23
24
|
export { ExactDecimal } from "./v1/json.js";
|
|
24
25
|
|
|
25
26
|
/** @typedef {import("./v1/transport.js").TerraBaseV1ClientConfig} TerraBaseV1ClientConfig */
|
package/src/local/better-auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateId, requireId } from "../ids.js";
|
|
2
2
|
import { createAdapterFactory } from "@better-auth/core/db/adapter";
|
|
3
3
|
/** @import { CleanedWhere, CustomAdapter, DBAdapter, JoinConfig } from "@better-auth/core/db/adapter" */
|
|
4
4
|
/** @import { BetterAuthOptions } from "@better-auth/core" */
|
|
@@ -29,6 +29,8 @@ export function createLocalBetterAuthAdapter(store) {
|
|
|
29
29
|
* @returns {DBAdapter<BetterAuthOptions>}
|
|
30
30
|
*/
|
|
31
31
|
function make(options, scoped) {
|
|
32
|
+
if (options.advanced?.database?.generateId === "uuid")
|
|
33
|
+
throw new TypeError("Better Auth entity IDs must be ULIDs; UUID generation is unavailable.");
|
|
32
34
|
/** @type {ReturnType<typeof createAdapterFactory<BetterAuthOptions>>} */
|
|
33
35
|
const factory = createAdapterFactory({
|
|
34
36
|
config: {
|
|
@@ -38,7 +40,7 @@ export function createLocalBetterAuthAdapter(store) {
|
|
|
38
40
|
supportsArrays: true,
|
|
39
41
|
supportsBooleans: true,
|
|
40
42
|
supportsNumericIds: false,
|
|
41
|
-
|
|
43
|
+
customIdGenerator: generateId,
|
|
42
44
|
transaction: async (callback) => {
|
|
43
45
|
if (scoped)
|
|
44
46
|
throw new Error("Nested local auth transactions are unavailable.");
|
|
@@ -65,7 +67,8 @@ function customAdapter(store, config, scoped) {
|
|
|
65
67
|
* @param {string} model
|
|
66
68
|
* @returns {Row[]}
|
|
67
69
|
*/
|
|
68
|
-
const rows = (model) => store.
|
|
70
|
+
const rows = (model) => store.listEntries("local-auth", model)
|
|
71
|
+
.map(entry => ({ .../** @type {Row} */ (entry.value), id: requireId(entry.id) }));
|
|
69
72
|
/**
|
|
70
73
|
* @param {string} model
|
|
71
74
|
* @param {readonly CleanedWhere[]} clauses
|
|
@@ -150,13 +153,21 @@ function customAdapter(store, config, scoped) {
|
|
|
150
153
|
*/
|
|
151
154
|
const save = (model, input, creating = false) => {
|
|
152
155
|
const row = jsonRow(input);
|
|
153
|
-
const id =
|
|
154
|
-
|
|
156
|
+
const id = row.id === undefined ? generateId() : requireId(row.id);
|
|
157
|
+
for (const [logical, table] of Object.entries(config.schema)) {
|
|
158
|
+
if (config.getModelName(logical) !== model) continue;
|
|
159
|
+
for (const [field, schema] of Object.entries(table.fields)) {
|
|
160
|
+
if (schema.references?.field !== "id") continue;
|
|
161
|
+
const value = row[config.getFieldName({ model: logical, field })];
|
|
162
|
+
if (value !== undefined && value !== null) requireId(value, field);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
155
165
|
const candidate = { ...row, id };
|
|
156
166
|
if (creating && store.get("local-auth", model, id) !== undefined)
|
|
157
167
|
throw new LocalAuthConstraintError();
|
|
158
168
|
ensureUnique(model, candidate);
|
|
159
|
-
|
|
169
|
+
delete row.id;
|
|
170
|
+
store.put("local-auth", model, id, row);
|
|
160
171
|
return candidate;
|
|
161
172
|
};
|
|
162
173
|
/**
|
package/src/local/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { generateId } from "../ids.js";
|
|
1
2
|
import { isAbsolute } from "node:path";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { createTerraBaseClient } from "./legacy-client.js";
|
|
4
4
|
import { normalizeLocalBearerToken } from "./authentication.js";
|
|
5
5
|
import { assertLocalSqliteEnvironment } from "./environment.js";
|
|
@@ -86,7 +86,7 @@ export class TerraScaleSqliteRuntime {
|
|
|
86
86
|
const scope =
|
|
87
87
|
options.scope ??
|
|
88
88
|
(stored === undefined
|
|
89
|
-
? { tenant:
|
|
89
|
+
? { tenant: generateId(), database: generateId(), shard: 0n }
|
|
90
90
|
: { ...stored, shard: BigInt(stored.shard) });
|
|
91
91
|
this.#router = new TerraScaleLocalRouter(this.#store, scope, {
|
|
92
92
|
authentication,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
decodeDatabaseCommitReceipt, decodeDatabasePoint, decodeDatabaseQueryPage,
|
|
4
4
|
decodeDatabaseSubscription, decodeDatabaseStatus, encodeDatabaseStatus, encodeDatabaseCommit, encodeDatabaseQuery,
|
|
5
|
-
encodeDatabaseSubscription, encodeDatabaseAuthority, parseDatabaseJson, requireDatabaseU64,
|
|
5
|
+
encodeDatabaseSubscription, encodeDatabaseAuthority, parseDatabaseJson, requireDatabaseU64, requireDatabaseId,
|
|
6
6
|
} from "../database-codec.js";
|
|
7
7
|
/**
|
|
8
8
|
* @import {
|
|
@@ -61,7 +61,7 @@ function identityAuthority(authority) {
|
|
|
61
61
|
* @returns {string}
|
|
62
62
|
*/
|
|
63
63
|
function scopePath(scope) {
|
|
64
|
-
return `/v1/tenants/${
|
|
64
|
+
return `/v1/tenants/${requireDatabaseId(scope.tenant, "tenant")}/databases/${requireDatabaseId(scope.database, "database")}/shards/${requireDatabaseU64(scope.shard, "shard")}`;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -195,7 +195,7 @@ export class TerraBaseClient {
|
|
|
195
195
|
|
|
196
196
|
/** @param {TerraBaseClientConfig} config */
|
|
197
197
|
constructor(config) {
|
|
198
|
-
this.#scope = Object.freeze({ tenant:
|
|
198
|
+
this.#scope = Object.freeze({ tenant: requireDatabaseId(config.tenant, "tenant"), database: requireDatabaseId(config.database, "database"), shard: requireDatabaseU64(config.shard, "shard") });
|
|
199
199
|
this.#config = { ...config };
|
|
200
200
|
this.#fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
201
201
|
this.#path = scopePath(this.#scope);
|
|
@@ -214,7 +214,7 @@ export class TerraBaseClient {
|
|
|
214
214
|
* @returns {Promise<DatabaseResult<DatabasePoint>>}
|
|
215
215
|
*/
|
|
216
216
|
read(collection, document, options = {}) {
|
|
217
|
-
const path = `${this.#collectionPath(collection)}/documents/${
|
|
217
|
+
const path = `${this.#collectionPath(collection)}/documents/${requireDatabaseId(document, "document")}`;
|
|
218
218
|
return this.#request(path, "GET", undefined, raw => this.#scopedPoint(decodeDatabasePoint(raw)), options);
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -312,7 +312,7 @@ export class TerraBaseClient {
|
|
|
312
312
|
* @param {string} collection
|
|
313
313
|
* @returns {string}
|
|
314
314
|
*/
|
|
315
|
-
#collectionPath(collection) { return `${this.#path}/collections/${
|
|
315
|
+
#collectionPath(collection) { return `${this.#path}/collections/${requireDatabaseId(collection, "collection")}`; }
|
|
316
316
|
|
|
317
317
|
/**
|
|
318
318
|
* @param {DatabasePoint} point
|
package/src/local/router.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { generateId } from "../ids.js";
|
|
1
2
|
import {
|
|
2
3
|
createHash,
|
|
3
4
|
createHmac,
|
|
4
5
|
randomBytes,
|
|
5
|
-
randomUUID,
|
|
6
6
|
timingSafeEqual,
|
|
7
7
|
} from "node:crypto";
|
|
8
8
|
import {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
encodeDatabaseValue,
|
|
19
19
|
parseDatabaseJson,
|
|
20
20
|
requireDatabaseU64,
|
|
21
|
-
|
|
21
|
+
requireDatabaseId,
|
|
22
22
|
} from "../database-codec.js";
|
|
23
23
|
import { matchesLocalBearerAuthorization } from "./authentication.js";
|
|
24
24
|
import { jsonResponse, problemResponse } from "./context.js";
|
|
@@ -101,8 +101,8 @@ export class TerraScaleLocalRouter {
|
|
|
101
101
|
constructor(store, scope, options) {
|
|
102
102
|
this.#store = store;
|
|
103
103
|
this.#scope = Object.freeze({
|
|
104
|
-
tenant:
|
|
105
|
-
database:
|
|
104
|
+
tenant: requireDatabaseId(scope.tenant),
|
|
105
|
+
database: requireDatabaseId(scope.database),
|
|
106
106
|
shard: requireDatabaseU64(scope.shard),
|
|
107
107
|
});
|
|
108
108
|
this.#scopeKey = `${scope.tenant}/${scope.database}/${scope.shard}`;
|
|
@@ -112,7 +112,7 @@ export class TerraScaleLocalRouter {
|
|
|
112
112
|
const writer = /** @type {{ readonly replica: string; readonly incarnation: string } | undefined} */ (
|
|
113
113
|
store.getMeta(`native-writer:${this.#scopeKey}`)
|
|
114
114
|
) ?? {
|
|
115
|
-
replica:
|
|
115
|
+
replica: generateId(),
|
|
116
116
|
incarnation: randomBytes(16).toString("hex"),
|
|
117
117
|
};
|
|
118
118
|
this.#writer = Object.freeze(writer);
|
|
@@ -131,7 +131,7 @@ export class TerraScaleLocalRouter {
|
|
|
131
131
|
* @returns {void}
|
|
132
132
|
*/
|
|
133
133
|
bindCollection(binding) {
|
|
134
|
-
|
|
134
|
+
requireDatabaseId(binding.collection);
|
|
135
135
|
requireDatabaseU64(binding.schema_generation, "schema generation", true);
|
|
136
136
|
if (!/^[0-9a-f]{64}$/u.test(binding.schema_digest))
|
|
137
137
|
throw new TypeError("Schema digest must be lowercase SHA256.");
|
|
@@ -170,8 +170,8 @@ export class TerraScaleLocalRouter {
|
|
|
170
170
|
suffix,
|
|
171
171
|
);
|
|
172
172
|
if (match === null) return problemResponse(404, "unknown_route");
|
|
173
|
-
const collection =
|
|
174
|
-
const document =
|
|
173
|
+
const collection = requireDatabaseId(match[1]);
|
|
174
|
+
const document = requireDatabaseId(match[2]);
|
|
175
175
|
return await this.#store.exclusive(() => {
|
|
176
176
|
this.#binding(collection);
|
|
177
177
|
return jsonResponse(this.#point(collection, document));
|
|
@@ -246,7 +246,7 @@ export class TerraScaleLocalRouter {
|
|
|
246
246
|
suffix,
|
|
247
247
|
);
|
|
248
248
|
if (match === null) return problemResponse(404, "unknown_route");
|
|
249
|
-
const collection =
|
|
249
|
+
const collection = requireDatabaseId(match[1]);
|
|
250
250
|
return await this.#store.exclusive(() => {
|
|
251
251
|
this.#binding(collection);
|
|
252
252
|
return this.#query(collection, match[2] === "subscriptions", raw);
|
|
@@ -599,7 +599,7 @@ export class TerraScaleLocalRouter {
|
|
|
599
599
|
let id;
|
|
600
600
|
let offset = 0;
|
|
601
601
|
if (cursorValue === undefined) {
|
|
602
|
-
id =
|
|
602
|
+
id = generateId();
|
|
603
603
|
const rows = /** @type {readonly { readonly scope: string; readonly id: string; readonly value: WirePoint }[]} */ (
|
|
604
604
|
this.#store.listNamespace("native-points")
|
|
605
605
|
)
|
|
@@ -812,10 +812,10 @@ function decodeCommit(raw) {
|
|
|
812
812
|
if (!Array.isArray(write.observed))
|
|
813
813
|
throw new TypeError("Invalid observed.");
|
|
814
814
|
return {
|
|
815
|
-
collection:
|
|
815
|
+
collection: requireDatabaseId(write.collection),
|
|
816
816
|
schema_generation: decimal(write.schema_generation),
|
|
817
817
|
schema_digest: text(write.schema_digest),
|
|
818
|
-
document:
|
|
818
|
+
document: requireDatabaseId(write.document),
|
|
819
819
|
condition: /** @type {DatabaseCondition} */ (parsedCondition),
|
|
820
820
|
observed: write.observed.map((rawObserved) => {
|
|
821
821
|
const observed = object(rawObserved);
|