@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/CHANGELOG.md +26 -0
- package/README.md +42 -15
- 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 +10 -0
- 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-ids.js +122 -0
- package/src/ts-auth.js +74 -72
- package/src/v1/decode.js +20 -8
- package/src/v1/ids.js +7 -60
- 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/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-ids.d.ts +14 -0
- package/types/ts-auth.d.ts +99 -99
- package/types/v1/decode.d.ts +8 -6
- 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/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 {
|
|
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]
|
|
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 {
|
|
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 =
|
|
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 =
|
|
106
|
+
const document = documentObject(raw.document);
|
|
100
107
|
if (raw.siblings === undefined) return Object.freeze({ id, version, state, document });
|
|
101
|
-
|
|
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:
|
|
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
|
|
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
|
|
22
|
+
* The catalog project ID is a canonical ULID; database and collection routes use names.
|
|
28
23
|
* @param {unknown} project
|
|
29
|
-
* @returns {
|
|
24
|
+
* @returns {import("../ids.js").EntityId}
|
|
30
25
|
*/
|
|
31
26
|
export function requireProject(project) {
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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}/
|
|
24
|
-
{ operation: "putDocument", method: "PUT", pathTemplate: `${documents}/
|
|
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}/
|
|
27
|
-
{ operation: "deleteDocument", method: "DELETE", pathTemplate: `${documents}/
|
|
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:
|
|
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
|
-
|
|
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:
|
|
108
|
+
return Object.freeze({ op: /** @type {const} */ ("upsert"), id, version: requireVersion(op.version), document: documentObject(op.document) });
|
|
110
109
|
})) });
|
|
111
110
|
})),
|
|
112
111
|
});
|
package/src/v1/terrabase-v1.js
CHANGED
|
@@ -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 {
|
|
273
|
+
* @returns {URLSearchParams}
|
|
263
274
|
*/
|
|
264
|
-
#
|
|
265
|
-
return {
|
|
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.#
|
|
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
|
|
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
|
|
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
|
|
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 {
|
package/src/v1/transport.js
CHANGED
|
@@ -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
|
-
|
|
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_
|
|
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
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* Entity identities are canonical uppercase ULIDs, including explicit local development.
|
|
3
3
|
*/
|
|
4
|
-
export type
|
|
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:
|
|
10
|
-
readonly database:
|
|
9
|
+
readonly tenant: EntityId;
|
|
10
|
+
readonly database: EntityId;
|
|
11
11
|
readonly shard: bigint;
|
|
12
12
|
};
|
|
13
13
|
export type CollectionBinding = {
|
|
14
|
-
readonly collection:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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";
|
package/types/local/store.d.ts
CHANGED
|
@@ -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:
|
|
9
|
+
collection: string;
|
|
10
10
|
binding: {
|
|
11
|
-
collection:
|
|
11
|
+
collection: string;
|
|
12
12
|
schema_generation: bigint;
|
|
13
13
|
schema_digest: string;
|
|
14
14
|
};
|
package/types/postgres.d.ts
CHANGED
|
@@ -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:
|
|
29
|
-
* readonly keyId:
|
|
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:
|
|
111
|
-
readonly keyId:
|
|
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;
|