@terrascale/sdk 0.0.2 → 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/src/v1/decode.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Typed decoding of TerraBase v1 response bodies.
2
- import { parseU64, requireVersion } from "./ids.js";
2
+ import { parseU64, requireDocumentId, requireVersion } from "./ids.js";
3
3
  import { isJsonObject } from "./json.js";
4
4
  /** @import { JsonObject, JsonValue } from "./json.js" */
5
5
 
@@ -8,17 +8,17 @@ import { isJsonObject } from "./json.js";
8
8
  * `document` is the value, or the LWW winner when a causal read answers `"conflict"` with `siblings`.
9
9
  * Unknown future states pass through.
10
10
  * @typedef {object} V1Document
11
- * @property {string} id
11
+ * @property {import("../ids.js").EntityId} id
12
12
  * @property {string} version
13
13
  * @property {"value" | "conflict" | (string & {})} state
14
14
  * @property {JsonObject} document
15
- * @property {readonly JsonValue[]} [siblings] Every live version, on a causal read with several.
15
+ * @property {readonly JsonValue[]} [siblings] Live versions and tombstones on a causal conflict.
16
16
  */
17
17
 
18
18
  /**
19
19
  * The result of a document write.
20
20
  * @typedef {object} V1WriteResult
21
- * @property {string} id
21
+ * @property {import("../ids.js").EntityId} id
22
22
  * @property {string} version For a delete, the tombstone's version.
23
23
  * @property {string} offset The database commit offset (decimal u64), usable with shape `waitFor`.
24
24
  * @property {V1Durability} durability
@@ -43,6 +43,13 @@ export function object(value, what) {
43
43
  return isJsonObject(value) ? value : malformed(what);
44
44
  }
45
45
 
46
+ /** @param {JsonValue | undefined} value @returns {JsonObject} */
47
+ export function documentObject(value) {
48
+ const document = object(value, "document body");
49
+ if (Object.hasOwn(document, "id")) malformed("document body: the entity ID belongs in the document key");
50
+ return document;
51
+ }
52
+
46
53
  /**
47
54
  * @param {JsonValue | undefined} value
48
55
  * @param {string} what
@@ -93,12 +100,17 @@ export function durability(value) {
93
100
  */
94
101
  export function decodeDocument(value) {
95
102
  const raw = object(value, "document");
96
- const id = string(raw.id, "document id");
103
+ const id = requireDocumentId(raw.id);
97
104
  const version = requireVersion(raw.version);
98
105
  const state = string(raw.state, "document state");
99
- const document = object(raw.document, "document body");
106
+ const document = documentObject(raw.document);
100
107
  if (raw.siblings === undefined) return Object.freeze({ id, version, state, document });
101
- return Object.freeze({ id, version, state, document, siblings: Object.freeze(array(raw.siblings, "document siblings")) });
108
+ const siblings = array(raw.siblings, "document siblings");
109
+ for (const sibling of siblings) {
110
+ const entry = object(sibling, "document sibling");
111
+ if (entry.deleted !== true || entry.document !== undefined) documentObject(entry.document);
112
+ }
113
+ return Object.freeze({ id, version, state, document, siblings: Object.freeze(siblings) });
102
114
  }
103
115
 
104
116
  /**
@@ -108,7 +120,7 @@ export function decodeDocument(value) {
108
120
  export function decodeWrite(response) {
109
121
  const raw = object(response.value, "write result");
110
122
  return Object.freeze({
111
- id: string(raw.id, "write id"),
123
+ id: requireDocumentId(raw.id),
112
124
  version: requireVersion(raw.version),
113
125
  offset: u64(raw.offset, "write offset"),
114
126
  durability: durability(raw.durability),
package/src/v1/ids.js CHANGED
@@ -1,4 +1,5 @@
1
- // Identifiers for the TerraBase v1 Documents API (api-v1.md).
1
+ // Identifiers for the TerraBase v1 Documents API.
2
+ import { requireId } from "../ids.js";
2
3
 
3
4
  const namePattern = /^[a-z][a-z0-9_-]{0,62}$/u;
4
5
  // Version tokens are opaque (tbv1-<32 hex>, tbv2-...): only RFC 9110 opaque-tag characters, safe to quote in If-Match.
@@ -6,12 +7,6 @@ const versionPattern = /^[\x21\x23-\x7e]{1,256}$/u;
6
7
  const offsetPattern = /^(?:0|[1-9]\d{0,19})$/u;
7
8
  const maximumU64 = (1n << 64n) - 1n;
8
9
 
9
- /** RFC 9562 NAMESPACE_URL, 6ba7b811-9dad-11d1-80b4-00c04fd430c8. */
10
- const namespaceUrl = Uint8Array.of(
11
- 0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1,
12
- 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8,
13
- );
14
-
15
10
  /**
16
11
  * @param {"database" | "collection"} kind
17
12
  * @param {unknown} name
@@ -24,24 +19,17 @@ export function requireName(kind, name) {
24
19
  }
25
20
 
26
21
  /**
27
- * The producer resolves this opaque catalog project id (normally a UUID), not its display name.
22
+ * The catalog project ID is a canonical ULID; database and collection routes use names.
28
23
  * @param {unknown} project
29
- * @returns {string}
24
+ * @returns {import("../ids.js").EntityId}
30
25
  */
31
26
  export function requireProject(project) {
32
- if (typeof project !== "string" || project === "" || project === "." || project === ".." || /[\p{Cc}\uD800-\uDFFF]/u.test(project))
33
- throw new TypeError("project must be a non-empty catalog project ID without control characters or dot path segments.");
34
- return project;
27
+ return requireId(project);
35
28
  }
36
29
 
37
- /**
38
- * @param {unknown} id
39
- * @returns {string}
40
- */
30
+ /** @param {unknown} id @returns {import("../ids.js").EntityId} */
41
31
  export function requireDocumentId(id) {
42
- if (typeof id !== "string" || id.length < 1 || id.length > 256 || /[\p{Cc}]/u.test(id) || /[\uD800-\uDFFF]/u.test(id) || id === "." || id === "..")
43
- throw new TypeError("Document IDs must be 1-256 characters without control characters or dot path segments; new IDs must be lowercase UUIDs.");
44
- return id;
32
+ return requireId(id);
45
33
  }
46
34
 
47
35
  /**
@@ -76,44 +64,3 @@ export function requireU64(value, what) {
76
64
  if (parsed === undefined) throw new TypeError(`${what} must be a u64: a canonical decimal string or bigint.`);
77
65
  return parsed;
78
66
  }
79
-
80
- /**
81
- * @param {Uint8Array} namespace
82
- * @param {string} name
83
- * @returns {Promise<Uint8Array>}
84
- */
85
- async function uuidV5(namespace, name) {
86
- const encoded = new TextEncoder().encode(name);
87
- const input = new Uint8Array(namespace.length + encoded.length);
88
- input.set(namespace);
89
- input.set(encoded, namespace.length);
90
- const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-1", input));
91
- const uuid = digest.slice(0, 16);
92
- uuid[6] = ((uuid[6] ?? 0) & 0x0f) | 0x50;
93
- uuid[8] = ((uuid[8] ?? 0) & 0x3f) | 0x80;
94
- return uuid;
95
- }
96
-
97
- /**
98
- * @param {Uint8Array} bytes
99
- * @returns {string}
100
- */
101
- function formatUuid(bytes) {
102
- const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
103
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
104
- }
105
-
106
- /**
107
- * The deterministic document ID for a natural key in a collection:
108
- * `uuidv5(uuidv5(NAMESPACE_URL, "terrabase:collection:" + collection), key)`.
109
- * Uses WebCrypto, so it works in Node.js, browsers and workers.
110
- * @param {string} collection
111
- * @param {string} key
112
- * @returns {Promise<string>}
113
- */
114
- export async function documentIdFor(collection, key) {
115
- requireName("collection", collection);
116
- if (typeof key !== "string") throw new TypeError("The natural key must be a string.");
117
- const namespace = await uuidV5(namespaceUrl, `terrabase:collection:${collection}`);
118
- return formatUuid(await uuidV5(namespace, key));
119
- }
package/src/v1/routes.js CHANGED
@@ -1,4 +1,4 @@
1
- // Routes owned by the C# Documents API at 0248ad3 (decision 0004 and shape-reads.md).
1
+ // Routes owned by the C# Documents API at 6cb9745 (decision 0004 and shape-reads.md).
2
2
  /** @typedef {"getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape"} V1Operation */
3
3
  /**
4
4
  * @typedef {object} V1Route
@@ -20,11 +20,11 @@ const collection = `${collections}/{collection}`;
20
20
  const documents = `${collection}/documents`;
21
21
  /** @type {readonly V1Route[]} */
22
22
  export const v1Routes = Object.freeze(/** @type {V1Route[]} */ ([
23
- { operation: "getDocument", method: "GET", pathTemplate: `${documents}/{id}`, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
24
- { operation: "putDocument", method: "PUT", pathTemplate: `${documents}/{id}`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [200, 201] },
23
+ { operation: "getDocument", method: "GET", pathTemplate: `${documents}/by-key`, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
24
+ { operation: "putDocument", method: "PUT", pathTemplate: `${documents}/by-key`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [200, 201] },
25
25
  { operation: "createDocument", method: "POST", pathTemplate: documents, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [201] },
26
- { operation: "patchDocument", method: "PATCH", pathTemplate: `${documents}/{id}`, permission: "documents:write", requestMediaTypes: ["application/merge-patch+json", "application/vnd.terrabase.ops+json"], successStatuses: [200] },
27
- { operation: "deleteDocument", method: "DELETE", pathTemplate: `${documents}/{id}`, permission: "documents:write", requestMediaTypes: [], successStatuses: [200] },
26
+ { operation: "patchDocument", method: "PATCH", pathTemplate: `${documents}/by-key`, permission: "documents:write", requestMediaTypes: ["application/merge-patch+json", "application/vnd.terrabase.ops+json"], successStatuses: [200] },
27
+ { operation: "deleteDocument", method: "DELETE", pathTemplate: `${documents}/by-key`, permission: "documents:write", requestMediaTypes: [], successStatuses: [200] },
28
28
  { operation: "commitTransaction", method: "POST", pathTemplate: `${database}/transactions`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [200] },
29
29
  { operation: "queryDocuments", method: "POST", pathTemplate: `${collection}/query`, permission: "documents:read", requestMediaTypes: ["application/json"], successStatuses: [200] },
30
30
  { operation: "createCollection", method: "POST", pathTemplate: collections, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [201] },
package/src/v1/shapes.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Current C# GET collection/shape contract (docs/shape-reads.md).
2
- import { array, boolean, malformed, object, string, u64 } from "./decode.js";
2
+ import { array, boolean, documentObject, malformed, object, string, u64 } from "./decode.js";
3
3
  import { requireFilter } from "./filter.js";
4
- import { parseU64, requireVersion } from "./ids.js";
4
+ import { parseU64, requireDocumentId, requireVersion } from "./ids.js";
5
5
  import { stringifyExactJson } from "./json.js";
6
6
  /**
7
7
  * @import { JsonObject, JsonValue } from "./json.js"
@@ -16,7 +16,7 @@ import { stringifyExactJson } from "./json.js";
16
16
  * @property {boolean | undefined} [live] True only for log positions; waits up to 20 seconds.
17
17
  * @property {string | undefined} [cursor] Previous live response's TB-Shape-Cursor.
18
18
  */
19
- /** @typedef {{ readonly op: "upsert"; readonly id: string; readonly version: string; readonly document: JsonObject } | { readonly op: "delete"; readonly id: string }} V1ShapeOp */
19
+ /** @typedef {{ readonly op: "upsert"; readonly id: import("../ids.js").EntityId; readonly version: string; readonly document: JsonObject } | { readonly op: "delete"; readonly id: import("../ids.js").EntityId }} V1ShapeOp */
20
20
  /** @typedef {{ readonly offset: string; readonly ops: readonly V1ShapeOp[] }} V1ShapeChange */
21
21
  /**
22
22
  * One snapshot page or log chunk. Only a fresh open tail/live response can report upToDate.
@@ -53,7 +53,7 @@ export function requireShapeOffset(value) {
53
53
  const bytes = Uint8Array.from(atob(encoded.replace(/-/gu, "+").replace(/_/gu, "/")), c => c.charCodeAt(0));
54
54
  const id = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
55
55
  const canonical = btoa(String.fromCharCode(...new TextEncoder().encode(id))).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "");
56
- if (canonical === encoded) return value;
56
+ if (canonical === encoded) { requireDocumentId(id); return value; }
57
57
  } catch { /* Invalid base64url or UTF-8. */ }
58
58
  }
59
59
  throw new TypeError("A shape offset is -1, a signed-64-bit decimal log position, or snap.<head>.<canonical-base64url-id>.");
@@ -102,11 +102,10 @@ export function decodeShapePage(value, headers) {
102
102
  const change = object(value, "shape change");
103
103
  return Object.freeze({ offset: u64(change.offset, "shape change offset"), ops: Object.freeze(array(change.ops, "shape operations").map(value => {
104
104
  const op = object(value, "shape operation");
105
- // Shape snapshots can include stored legacy ids beyond the HTTP point-id limit.
106
- const id = string(op.id, "shape document id");
105
+ const id = requireDocumentId(op.id);
107
106
  if (op.op === "delete") return Object.freeze({ op: /** @type {const} */ ("delete"), id });
108
107
  if (op.op !== "upsert") malformed("shape operation kind");
109
- return Object.freeze({ op: /** @type {const} */ ("upsert"), id, version: requireVersion(op.version), document: object(op.document, "shape document") });
108
+ return Object.freeze({ op: /** @type {const} */ ("upsert"), id, version: requireVersion(op.version), document: documentObject(op.document) });
110
109
  })) });
111
110
  })),
112
111
  });
@@ -94,6 +94,8 @@ function pointer(path) {
94
94
  */
95
95
  function patchOp(op) {
96
96
  if (typeof op !== "object" || op === null || Array.isArray(op)) throw new TypeError("A patch op is an object.");
97
+ if (op.path === "/id" || op.path?.startsWith("/id/")) throw new TypeError("The document key is the entity ID; payloads must not contain an id field.");
98
+ if (op.path === "" && op.op === "set") assertNoPayloadId(op.value);
97
99
  let allowed = ["op", "path", "value"];
98
100
  switch (op.op) {
99
101
  case "unset": allowed = ["op", "path"]; break;
@@ -133,6 +135,7 @@ function normalizePatch(patch) {
133
135
  if (hasMerge) {
134
136
  const merge = /** @type {{ readonly merge: JsonInputObject }} */ (patch).merge;
135
137
  assertJsonObject(merge, "merge patch");
138
+ assertNoPayloadId(merge);
136
139
  return { merge };
137
140
  }
138
141
  const ops = /** @type {{ readonly ops: readonly V1PatchOp[] }} */ (patch).ops;
@@ -228,11 +231,18 @@ function rejectIdempotency(options) {
228
231
  /** @param {JsonInputObject} document @returns {string} */
229
232
  function encodeDocument(document) {
230
233
  assertJsonObject(document, "document");
234
+ assertNoPayloadId(document);
231
235
  const body = stringifyExactJson(document, "document");
232
236
  if (new TextEncoder().encode(body).byteLength > 1024 * 1024) throw new TypeError("A document is at most 1 MiB.");
233
237
  return body;
234
238
  }
235
239
 
240
+ /** @param {unknown} document @returns {void} */
241
+ function assertNoPayloadId(document) {
242
+ if (typeof document === "object" && document !== null && Object.hasOwn(document, "id"))
243
+ throw new TypeError("The document key is the entity ID; payloads must not contain an id field.");
244
+ }
245
+
236
246
  /** A collection handle by name. It performs no I/O until an operation runs. */
237
247
  export class V1Collection {
238
248
  /** @type {V1Transport} */
@@ -258,11 +268,12 @@ export class V1Collection {
258
268
  }
259
269
 
260
270
  /**
271
+ * The by-key route takes the raw document ID in the query, not the path.
261
272
  * @param {string} id
262
- * @returns {Readonly<Record<string, string>>}
273
+ * @returns {URLSearchParams}
263
274
  */
264
- #document(id) {
265
- return { ...this.#params, id: requireDocumentId(id) };
275
+ #documentKey(id) {
276
+ return new URLSearchParams({ id: requireDocumentId(id) });
266
277
  }
267
278
 
268
279
  /**
@@ -276,7 +287,7 @@ export class V1Collection {
276
287
  /** @type {Record<string, string>} */
277
288
  const headers = {};
278
289
  if (options.ifNoneMatch !== undefined) headers["if-none-match"] = `"${requireVersion(options.ifNoneMatch)}"`;
279
- const response = await this.#transport.call("getDocument", this.#document(id), { headers, signal: options.signal });
290
+ const response = await this.#transport.call("getDocument", this.#params, { query: this.#documentKey(id), headers, signal: options.signal });
280
291
  return decodeDocument(response.value);
281
292
  }
282
293
 
@@ -288,15 +299,14 @@ export class V1Collection {
288
299
  * @returns {Promise<V1WriteResult>}
289
300
  */
290
301
  async put(id, document, options = {}) {
291
- const params = this.#document(id);
292
302
  const headers = preconditionHeaders(options, true);
293
- return decodeWrite(await this.#transport.call("putDocument", params, {
294
- body: encodeDocument(document), headers, signal: options.signal,
303
+ return decodeWrite(await this.#transport.call("putDocument", this.#params, {
304
+ query: this.#documentKey(id), body: encodeDocument(document), headers, signal: options.signal,
295
305
  }));
296
306
  }
297
307
 
298
308
  /**
299
- * Creates a document under a server-generated UUIDv7.
309
+ * Creates a document under a server-generated ULID.
300
310
  * @param {JsonInputObject} document
301
311
  * @param {V1CreateOptions} [options]
302
312
  * @returns {Promise<V1WriteResult>}
@@ -315,14 +325,13 @@ export class V1Collection {
315
325
  * @returns {Promise<V1WriteResult>}
316
326
  */
317
327
  async patch(id, patch, options = {}) {
318
- const params = this.#document(id);
319
328
  const headers = preconditionHeaders(options, false);
320
329
  const normalized = normalizePatch(patch);
321
330
  const [contentType, body] = "merge" in normalized
322
331
  ? ["application/merge-patch+json", stringifyExactJson(normalized.merge, "merge patch")]
323
332
  : ["application/vnd.terrabase.ops+json", stringifyExactJson({ ops: normalized.ops }, "patch ops")];
324
- return decodeWrite(await this.#transport.call("patchDocument", params, {
325
- body, contentType, headers,
333
+ return decodeWrite(await this.#transport.call("patchDocument", this.#params, {
334
+ query: this.#documentKey(id), body, contentType, headers,
326
335
  signal: options.signal,
327
336
  }));
328
337
  }
@@ -334,10 +343,9 @@ export class V1Collection {
334
343
  * @returns {Promise<V1WriteResult>}
335
344
  */
336
345
  async delete(id, options = {}) {
337
- const params = this.#document(id);
338
346
  const headers = preconditionHeaders(options, false);
339
- return decodeWrite(await this.#transport.call("deleteDocument", params, {
340
- headers, signal: options.signal,
347
+ return decodeWrite(await this.#transport.call("deleteDocument", this.#params, {
348
+ query: this.#documentKey(id), headers, signal: options.signal,
341
349
  }));
342
350
  }
343
351
 
@@ -459,7 +467,7 @@ export class V1Transaction {
459
467
 
460
468
  /**
461
469
  * Adds a create (a put requiring absence). Omit `id` to have the server
462
- * generate a UUIDv7, returned in the op's result.
470
+ * generate a ULID, returned in the op's result.
463
471
  * @param {string} collection
464
472
  * @param {JsonInputObject} document
465
473
  * @param {string} [id]
@@ -690,7 +698,7 @@ export class V1Database {
690
698
  }
691
699
 
692
700
  /**
693
- * A TerraBase v1 data-plane client for one project and `tb_` API key. The key is held
701
+ * A TerraBase v1 data-plane client for one project and access token. The token is held
694
702
  * privately; it is never serialized, logged or forwarded to redirects.
695
703
  */
696
704
  export class TerraBaseV1Client {
@@ -15,7 +15,9 @@ import { v1Route, v1RoutePath } from "./routes.js";
15
15
  export const maximumRequestBytes = 4 * 1024 * 1024;
16
16
  // Query budgets count stored JSON before wrappers and serializer escaping.
17
17
  const defaultMaximumResponseBytes = 64 * 1024 * 1024;
18
- const tokenPattern = /^tb_[A-Za-z0-9_-]{43}$/u;
18
+ // Validate header-safe credential syntax only; the producer validates JWT claims/signatures.
19
+ // Retain C# keys while the published TS-589 JWT contract awaits a producer release.
20
+ const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|tb_[A-Za-z0-9_-]{43})$/u;
19
21
  /**
20
22
  * Resolver input contains no credential. A successful URL is cached for this client/database.
21
23
  * @typedef {{ readonly project: string; readonly database: string; readonly signal?: AbortSignal | undefined }} V1EndpointContext
@@ -26,7 +28,7 @@ const tokenPattern = /^tb_[A-Za-z0-9_-]{43}$/u;
26
28
  * URLs must use HTTPS or loopback HTTP. Mutations are never automatically replayed.
27
29
  * @typedef {object} TerraBaseV1ClientConfig
28
30
  * @property {string} project
29
- * @property {string} token A tb_ API key; held privately.
31
+ * @property {string} token A ts-auth project access JWT (or a legacy C# tb_ key); held privately.
30
32
  * @property {string | undefined} [baseUrl]
31
33
  * @property {V1EndpointResolver | undefined} [endpointResolver]
32
34
  * @property {FetchLike | undefined} [fetch]
@@ -123,7 +125,7 @@ export class BodyTransportError extends Error {
123
125
  }
124
126
  }
125
127
 
126
- /** Internal transport bound to one data key. */
128
+ /** Internal transport bound to one project credential. */
127
129
  export class V1Transport {
128
130
  /** @type {string} */
129
131
  #project;
@@ -148,7 +150,7 @@ export class V1Transport {
148
150
  throw new TypeError("endpointResolver must be a function.");
149
151
  this.#resolver = config.endpointResolver ?? staticBaseUrlResolver(/** @type {string} */ (config.baseUrl));
150
152
  if (typeof config.token !== "string" || !tokenPattern.test(config.token))
151
- throw new TypeError("token must be a tb_ API key with 43 base64url characters.");
153
+ throw new TypeError("token must be a compact access JWT or a legacy C# tb_ API key.");
152
154
  if ("autoIdempotencyKey" in config || "maxRetries" in config || "resolver" in config)
153
155
  throw new TypeError("Unsupported client configuration member.");
154
156
  const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
@@ -1,9 +1,3 @@
1
- /**
2
- * @param {unknown} value
3
- * @param {string} [name]
4
- * @returns {string}
5
- */
6
- export function requireDatabaseUuid(value: unknown, name?: string): string;
7
1
  /**
8
2
  * @param {unknown} value
9
3
  * @param {string} [name]
@@ -88,6 +82,7 @@ export function encodeDatabaseCommit(scope: DatabaseScope, commit: DatabaseCommi
88
82
  readonly body: string;
89
83
  readonly effect_digest: string;
90
84
  }>;
85
+ export { requireId as requireDatabaseId } from "./ids.js";
91
86
  export type Budget = {
92
87
  nodes: number;
93
88
  };
@@ -1,17 +1,17 @@
1
1
  /**
2
- * The current TerraBase document protocol uses canonical UUID entity IDs.
2
+ * Entity identities are canonical uppercase ULIDs, including explicit local development.
3
3
  */
4
- export type CanonicalUuid = string;
4
+ export type EntityId = import("./ids.js").EntityId;
5
5
  /**
6
6
  * All 64-bit quantities stay bigint in JavaScript and decimal strings on the wire.
7
7
  */
8
8
  export type DatabaseScope = {
9
- readonly tenant: CanonicalUuid;
10
- readonly database: CanonicalUuid;
9
+ readonly tenant: EntityId;
10
+ readonly database: EntityId;
11
11
  readonly shard: bigint;
12
12
  };
13
13
  export type CollectionBinding = {
14
- readonly collection: CanonicalUuid;
14
+ readonly collection: EntityId;
15
15
  readonly schema_generation: bigint;
16
16
  readonly schema_digest: string;
17
17
  };
@@ -44,12 +44,12 @@ export type DatabaseField = {
44
44
  readonly value: DatabaseValue;
45
45
  };
46
46
  export type DatabaseObserved = {
47
- readonly replica: CanonicalUuid;
47
+ readonly replica: EntityId;
48
48
  readonly incarnation: string;
49
49
  readonly sequence: bigint;
50
50
  };
51
51
  export type DatabaseEvent = DatabaseObserved & {
52
- readonly database: CanonicalUuid;
52
+ readonly database: EntityId;
53
53
  readonly shard: bigint;
54
54
  };
55
55
  export type DatabaseAuthority = ({
@@ -58,7 +58,7 @@ export type DatabaseAuthority = ({
58
58
  readonly mode: "single_writer";
59
59
  readonly generation: bigint;
60
60
  readonly writer: {
61
- readonly replica: CanonicalUuid;
61
+ readonly replica: EntityId;
62
62
  readonly incarnation: string;
63
63
  };
64
64
  });
@@ -77,7 +77,7 @@ export type DatabaseWriteValue = ({
77
77
  readonly kind: "tombstone";
78
78
  });
79
79
  export type DatabaseWrite = CollectionBinding & {
80
- readonly document: CanonicalUuid;
80
+ readonly document: EntityId;
81
81
  readonly condition: DatabaseCondition;
82
82
  readonly observed: readonly DatabaseObserved[];
83
83
  readonly value: DatabaseWriteValue;
@@ -121,7 +121,7 @@ export type DatabaseQuery = {
121
121
  readonly cursor?: string;
122
122
  };
123
123
  export type DatabaseQueryRow = {
124
- readonly document: CanonicalUuid;
124
+ readonly document: EntityId;
125
125
  readonly point: DatabasePoint;
126
126
  };
127
127
  export type DatabaseQueryPage = {
@@ -188,7 +188,7 @@ export type DatabaseSubscriptionRequest = ({
188
188
  readonly poll_cursor: string;
189
189
  });
190
190
  export type DatabaseSubscriptionEffect = {
191
- readonly document: CanonicalUuid;
191
+ readonly document: EntityId;
192
192
  readonly point: DatabasePoint | null;
193
193
  };
194
194
  export type DatabaseSubscriptionGroup = {
package/types/ids.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** @returns {EntityId} Monotonic within a millisecond, including clock rollback. */
2
+ export function generateId(): EntityId;
3
+ /** @param {unknown} value @returns {value is EntityId} */
4
+ export function isValidId(value: unknown): value is EntityId;
5
+ /** @param {unknown} value @param {string} [name] @returns {EntityId} */
6
+ export function requireId(value: unknown, name?: string): EntityId;
7
+ /**
8
+ * A plain 26-character, uppercase Crockford ULID.
9
+ */
10
+ export type EntityId = string;
package/types/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { v1Filter } from "./v1/filter.js";
2
- export { documentIdFor } from "./v1/ids.js";
3
2
  export { ExactDecimal } from "./v1/json.js";
4
3
  export { staticBaseUrlResolver } from "./v1/transport.js";
4
+ export type EntityId = import("./ids.js").EntityId;
5
5
  export type TerraBaseV1ClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
6
6
  export type V1RequestOptions = import("./v1/terrabase-v1.js").V1RequestOptions;
7
7
  export type V1ReadOptions = import("./v1/terrabase-v1.js").V1ReadOptions;
@@ -41,3 +41,4 @@ export type V1ShapePage = import("./v1/shapes.js").V1ShapePage;
41
41
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
42
42
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
43
43
  export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
44
+ export { generateId, isValidId, requireId } from "./ids.js";
@@ -34,6 +34,16 @@ export class TerraScaleSqliteStore {
34
34
  * @returns {T[]}
35
35
  */
36
36
  list<T>(namespace: string, scope: string): T[];
37
+ /**
38
+ * @template T
39
+ * @param {string} namespace
40
+ * @param {string} scope
41
+ * @returns {readonly { readonly id: string; readonly value: T }[]}
42
+ */
43
+ listEntries<T>(namespace: string, scope: string): readonly {
44
+ readonly id: string;
45
+ readonly value: T;
46
+ }[];
37
47
  /**
38
48
  * @template T
39
49
  * @param {string} namespace
@@ -6,9 +6,9 @@
6
6
  export function localFixture(options?: Omit<TerraScaleSqliteRuntimeOptions, "sqlite">): Promise<{
7
7
  runtime: import("./index.js").TerraScaleSqliteRuntime;
8
8
  client: import("./legacy-client.js").TerraBaseClient;
9
- collection: `${string}-${string}-${string}-${string}-${string}`;
9
+ collection: string;
10
10
  binding: {
11
- collection: `${string}-${string}-${string}-${string}-${string}`;
11
+ collection: string;
12
12
  schema_generation: bigint;
13
13
  schema_digest: string;
14
14
  };
@@ -25,8 +25,8 @@ export function postgresQuery(statement: string, parameters?: readonly unknown[]
25
25
  * readonly port: number;
26
26
  * readonly tlsServerName: string;
27
27
  * readonly caCertificatePem: string;
28
- * readonly database: string;
29
- * readonly keyId: string;
28
+ * readonly database: import("./ids.js").EntityId;
29
+ * readonly keyId: import("./ids.js").EntityId;
30
30
  * readonly password: string;
31
31
  * readonly connectTimeoutMs?: number;
32
32
  * }} PostgresConfig
@@ -107,8 +107,8 @@ export type PostgresConfig = {
107
107
  readonly port: number;
108
108
  readonly tlsServerName: string;
109
109
  readonly caCertificatePem: string;
110
- readonly database: string;
111
- readonly keyId: string;
110
+ readonly database: import("./ids.js").EntityId;
111
+ readonly keyId: import("./ids.js").EntityId;
112
112
  readonly password: string;
113
113
  readonly connectTimeoutMs?: number;
114
114
  };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Validate the documented endpointId:eventId delivery selector.
3
+ * @param {unknown} value
4
+ * @returns {string}
5
+ */
6
+ export function requireDeadLetterId(value: unknown): string;
7
+ /**
8
+ * Validate identity fields in the hosted-auth DTOs without inspecting customer metadata.
9
+ * @param {unknown} value
10
+ * @param {string} route
11
+ * @param {boolean} [entity]
12
+ * @returns {void}
13
+ */
14
+ export function validateTsAuthIds(value: unknown, route: string, entity?: boolean): void;