@terrascale/sdk 0.0.3 → 0.0.5
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 +31 -0
- package/README.md +206 -67
- package/package.json +10 -4
- package/sdk-public-v1-route-manifest.json +85 -0
- package/sdk-v1-route-manifest.json +896 -15
- package/src/http.js +3 -48
- package/src/index.js +80 -1
- package/src/local/environment.js +0 -1
- package/src/ts-auth.js +1 -5
- package/src/v1/backups.js +111 -0
- package/src/v1/catalog.js +240 -0
- package/src/v1/changes.js +225 -0
- package/src/v1/database-schema.js +45 -0
- package/src/v1/decode.js +6 -5
- package/src/v1/errors.js +23 -3
- package/src/v1/export.js +68 -0
- package/src/v1/ids.js +5 -2
- package/src/v1/imports.js +72 -0
- package/src/v1/indexes.js +76 -0
- package/src/v1/json.js +4 -3
- package/src/v1/management-decode.js +24 -0
- package/src/v1/management-routes.js +72 -0
- package/src/v1/management-validation.js +25 -0
- package/src/v1/routes.js +32 -7
- package/src/v1/saved-queries.js +58 -0
- package/src/v1/shape-crypto.js +125 -0
- package/src/v1/shape-manifest.js +31 -0
- package/src/v1/shape-reader.js +214 -0
- package/src/v1/shape-subscription.js +208 -0
- package/src/v1/shapes.js +11 -5
- package/src/v1/sql.js +98 -0
- package/src/v1/system.js +80 -0
- package/src/v1/terrabase-v1.js +231 -30
- package/src/v1/transport.js +117 -33
- package/src/v1/usage.js +101 -0
- package/types/http.d.ts +2 -8
- package/types/index.d.ts +73 -0
- package/types/local/environment.d.ts +1 -1
- package/types/ts-auth.d.ts +0 -2
- package/types/v1/backups.d.ts +128 -0
- package/types/v1/catalog.d.ts +104 -0
- package/types/v1/changes.d.ts +53 -0
- package/types/v1/database-schema.d.ts +82 -0
- package/types/v1/decode.d.ts +20 -4
- package/types/v1/errors.d.ts +16 -1
- package/types/v1/export.d.ts +26 -0
- package/types/v1/ids.d.ts +2 -2
- package/types/v1/imports.d.ts +132 -0
- package/types/v1/indexes.d.ts +75 -0
- package/types/v1/management-decode.d.ts +15 -0
- package/types/v1/management-routes.d.ts +3 -0
- package/types/v1/management-validation.d.ts +8 -0
- package/types/v1/routes.d.ts +5 -2
- package/types/v1/saved-queries.d.ts +33 -0
- package/types/v1/shape-crypto.d.ts +39 -0
- package/types/v1/shape-manifest.d.ts +28 -0
- package/types/v1/shape-reader.d.ts +45 -0
- package/types/v1/shape-subscription.d.ts +35 -0
- package/types/v1/shapes.d.ts +16 -2
- package/types/v1/sql.d.ts +47 -0
- package/types/v1/system.d.ts +85 -0
- package/types/v1/terrabase-v1.d.ts +140 -6
- package/types/v1/transport.d.ts +33 -10
- package/types/v1/usage.d.ts +171 -0
- package/src/ts-auth-discovery.js +0 -207
- package/types/ts-auth-discovery.d.ts +0 -19
package/src/v1/system.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { decodeDocument } from "./decode.js";
|
|
2
|
+
import { requireId } from "../ids.js";
|
|
3
|
+
import { requireDocumentId, requireName } from "./ids.js";
|
|
4
|
+
import { flag, list, nullable, numeric, record, text } from "./management-decode.js";
|
|
5
|
+
import { members } from "./management-validation.js";
|
|
6
|
+
import { decodeCollection, decodeQueryPage } from "./terrabase-v1.js";
|
|
7
|
+
import { V1Transport } from "./transport.js";
|
|
8
|
+
/** @import { V1RequestOptions } from "./terrabase-v1.js" */
|
|
9
|
+
const status = record({ status: text, version: text, region: text, storageFormat: text, revision: nullable(text), machine: text });
|
|
10
|
+
const replication = record({ region: text, status: text, maxLagSeconds: nullable(numeric),
|
|
11
|
+
peers: list(record({ region: text, status: text, lagSeconds: nullable(numeric) })) });
|
|
12
|
+
const databases = record({ items: list(record({ name: text, id: requireId, homeRegion: text, replicaRegions: list(text), allRegions: flag })) });
|
|
13
|
+
/** @typedef {ReturnType<typeof status> & {readonly httpStatus: number}} V1ProbeStatus */
|
|
14
|
+
/** @typedef {ReturnType<typeof replication>} V1ReplicationHealth */
|
|
15
|
+
/** @typedef {ReturnType<typeof databases>} V1PlatformDatabases */
|
|
16
|
+
/** @typedef {{ readonly baseUrl: string; readonly token?: string; readonly fetch?: import("../config.js").FetchLike; readonly maxResponseBytes?: number }} TerraBaseSystemClientConfig */
|
|
17
|
+
|
|
18
|
+
/** Public anonymous probes; 503 readiness/backup bodies remain typed probe outcomes. */
|
|
19
|
+
export class V1Health {
|
|
20
|
+
/** @type {V1Transport} */
|
|
21
|
+
#transport;
|
|
22
|
+
/** @param {V1Transport} transport */
|
|
23
|
+
constructor(transport) { this.#transport = transport; Object.freeze(this); }
|
|
24
|
+
/** @param {V1RequestOptions} [options] @returns {Promise<V1ProbeStatus>} */
|
|
25
|
+
async liveness(options = {}) {
|
|
26
|
+
const response = await this.#transport.call("healthz", {}, { signal: options.signal });
|
|
27
|
+
return Object.freeze({ ...status(response.value), httpStatus: response.status });
|
|
28
|
+
}
|
|
29
|
+
/** @param {V1RequestOptions} [options] @returns {Promise<V1ProbeStatus>} */
|
|
30
|
+
async readiness(options = {}) {
|
|
31
|
+
const response = await this.#transport.call("readyz", {}, { signal: options.signal });
|
|
32
|
+
return Object.freeze({ ...status(response.value), httpStatus: response.status });
|
|
33
|
+
}
|
|
34
|
+
/** @param {V1RequestOptions} [options] @returns {Promise<V1ProbeStatus>} */
|
|
35
|
+
async backup(options = {}) {
|
|
36
|
+
const response = await this.#transport.call("backupz", {}, { signal: options.signal });
|
|
37
|
+
return Object.freeze({ ...status(response.value), httpStatus: response.status });
|
|
38
|
+
}
|
|
39
|
+
/** @param {V1RequestOptions} [options] @returns {Promise<V1ReplicationHealth>} */
|
|
40
|
+
async replication(options = {}) { return replication((await this.#transport.call("replicationHealth", {}, { signal: options.signal })).value); }
|
|
41
|
+
}
|
|
42
|
+
/** The public read-only explore API requires a user JWT carrying platform_role=admin. */
|
|
43
|
+
export class V1Platform {
|
|
44
|
+
/** @type {V1Transport} */
|
|
45
|
+
#transport;
|
|
46
|
+
/** @param {V1Transport} transport */
|
|
47
|
+
constructor(transport) { this.#transport = transport; Object.freeze(this); }
|
|
48
|
+
/** @param {V1RequestOptions} [options] @returns {Promise<V1PlatformDatabases>} */
|
|
49
|
+
async databases(options = {}) { return databases((await this.#transport.call("listPlatformDatabases", {}, { signal: options.signal })).value); }
|
|
50
|
+
/** @param {"ts-auth" | "ts-dashboard"} database @param {V1RequestOptions} [options] */
|
|
51
|
+
async collections(database, options = {}) {
|
|
52
|
+
const response = await this.#transport.call("listPlatformCollections", { database: requireName("database", database) }, { signal: options.signal });
|
|
53
|
+
return record({ items: list(decodeCollection), next_cursor: nullable(text) })(response.value);
|
|
54
|
+
}
|
|
55
|
+
/** @param {"ts-auth" | "ts-dashboard"} database @param {string} collection @param {{ readonly limit?: number; readonly cursor?: string }} [request] @param {V1RequestOptions} [options] */
|
|
56
|
+
async documents(database, collection, request = {}, options = {}) {
|
|
57
|
+
members(request, ["limit", "cursor"]);
|
|
58
|
+
const query = new URLSearchParams();
|
|
59
|
+
if (request.limit !== undefined) {
|
|
60
|
+
if (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 1000) throw new TypeError("limit must be 1-1000.");
|
|
61
|
+
query.set("limit", String(request.limit));
|
|
62
|
+
}
|
|
63
|
+
if (request.cursor !== undefined) {
|
|
64
|
+
if (typeof request.cursor !== "string" || request.cursor === "") throw new TypeError("cursor must be non-empty.");
|
|
65
|
+
query.set("cursor", request.cursor);
|
|
66
|
+
}
|
|
67
|
+
return decodeQueryPage((await this.#transport.call("listPlatformDocuments", { database: requireName("database", database), collection: requireName("collection", collection) }, { query, signal: options.signal })).value);
|
|
68
|
+
}
|
|
69
|
+
/** @param {"ts-auth" | "ts-dashboard"} database @param {string} collection @param {string} id @param {V1RequestOptions} [options] */
|
|
70
|
+
async document(database, collection, id, options = {}) {
|
|
71
|
+
return decodeDocument((await this.#transport.call("getPlatformDocument", { database: requireName("database", database), collection: requireName("collection", collection) }, {
|
|
72
|
+
query: new URLSearchParams({ id: requireDocumentId(id) }), signal: options.signal,
|
|
73
|
+
})).value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** @param {TerraBaseSystemClientConfig} config */
|
|
77
|
+
export function createTerraBaseSystemClient(config) {
|
|
78
|
+
const transport = new V1Transport(config);
|
|
79
|
+
return Object.freeze({ health: new V1Health(transport), platform: new V1Platform(transport) });
|
|
80
|
+
}
|
package/src/v1/terrabase-v1.js
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
// The TerraBase v1 client: documents, transactions, queries and shapes over
|
|
2
2
|
// the project-scoped C# Documents API (decision 0004 and shape-reads.md).
|
|
3
3
|
import {
|
|
4
|
-
array, boolean, decodeDocument, decodeWrite, durability, malformed, object, u64,
|
|
4
|
+
array, boolean, decodeDocument, decodeWrite, documentObject, durability, malformed, object, u64,
|
|
5
5
|
} from "./decode.js";
|
|
6
|
+
import { requireId } from "../ids.js";
|
|
6
7
|
import { isJsonPointer, requireFilter, requirePath } from "./filter.js";
|
|
7
8
|
import { requireDocumentId, requireName, requireVersion } from "./ids.js";
|
|
8
9
|
import { assertJsonObject, assertJsonValue, ExactDecimal, stringifyExactJson } from "./json.js";
|
|
9
|
-
import {
|
|
10
|
+
import { readShapeManifest } from "./shape-manifest.js";
|
|
11
|
+
import { V1ShapeSubscription } from "./shape-subscription.js";
|
|
12
|
+
import { decryptSealedShapePage, readShapePage, readShapeKey } from "./shape-reader.js";
|
|
13
|
+
import { runSql, checkSql } from "./sql.js";
|
|
14
|
+
import { streamChanges } from "./changes.js";
|
|
15
|
+
import { exportDocuments } from "./export.js";
|
|
16
|
+
import { V1Databases, getCollectionSchema, putCollectionSchema, deleteCollectionSchema, getCollectionTtl, putCollectionTtl, deleteCollectionTtl } from "./catalog.js";
|
|
10
17
|
import { V1Transport } from "./transport.js";
|
|
18
|
+
import { V1Health, V1Platform } from "./system.js";
|
|
19
|
+
import { V1Imports } from "./imports.js";
|
|
20
|
+
import { V1Backups, V1BackupDestinationClient } from "./backups.js";
|
|
21
|
+
import { V1ProjectUsageClient, V1DatabaseUsageClient } from "./usage.js";
|
|
22
|
+
import { V1Indexes } from "./indexes.js";
|
|
23
|
+
import { V1DatabaseSchemaClient } from "./database-schema.js";
|
|
24
|
+
import { V1SavedQueries } from "./saved-queries.js";
|
|
11
25
|
/**
|
|
12
26
|
* @import { V1Document, V1Durability, V1WriteResult } from "./decode.js"
|
|
13
27
|
* @import { V1Filter, V1Path } from "./filter.js"
|
|
@@ -37,6 +51,7 @@ import { V1Transport } from "./transport.js";
|
|
|
37
51
|
* @typedef {{ readonly op: "set"; readonly path: string; readonly value: JsonInput }
|
|
38
52
|
* | { readonly op: "unset"; readonly path: string }
|
|
39
53
|
* | { readonly op: "inc"; readonly path: string; readonly by: number | bigint | ExactDecimal }
|
|
54
|
+
* | { readonly op: "add" | "remove"; readonly path: string; readonly value: import("./filter.js").V1Scalar }
|
|
40
55
|
* | { readonly op: "append"; readonly path: string; readonly value: JsonInput }} V1PatchOp
|
|
41
56
|
*/
|
|
42
57
|
/**
|
|
@@ -44,12 +59,17 @@ import { V1Transport } from "./transport.js";
|
|
|
44
59
|
* @typedef {{ readonly merge: JsonInputObject } | { readonly ops: readonly V1PatchOp[] }} V1Patch
|
|
45
60
|
*/
|
|
46
61
|
|
|
47
|
-
/** @typedef {{ readonly path: V1Path; readonly order?: "asc" | "desc" | undefined }}
|
|
62
|
+
/** @typedef {{ readonly path: V1Path; readonly order?: "asc" | "desc" | undefined }} V1FieldSortKey */
|
|
63
|
+
/**
|
|
64
|
+
* Vector similarity sort, nearest first. It must be the only sort key; limit is at most 100 and there is no cursor.
|
|
65
|
+
* @typedef {{ readonly near: { readonly path: V1Path; readonly vector: readonly number[]; readonly metric?: "cosine" | "l2" | undefined } }} V1NearSortKey
|
|
66
|
+
*/
|
|
67
|
+
/** @typedef {V1FieldSortKey | V1NearSortKey} V1SortKey */
|
|
48
68
|
/**
|
|
49
69
|
* A query-v1 request. Every member is optional.
|
|
50
70
|
* @typedef {object} V1Query
|
|
51
71
|
* @property {V1Filter | null | undefined} [filter]
|
|
52
|
-
* @property {readonly V1SortKey[] | undefined} [sort] At most 4 keys; results are finally ordered by `$id`.
|
|
72
|
+
* @property {readonly V1SortKey[] | undefined} [sort] At most 4 keys, or one `near` key; results are finally ordered by `$id`.
|
|
53
73
|
* @property {readonly string[] | undefined} [project] At most 64 paths.
|
|
54
74
|
* @property {number | undefined} [limit] 1-1000 (server default 50).
|
|
55
75
|
* @property {string | null | undefined} [cursor] A previous page's `nextCursor`.
|
|
@@ -67,7 +87,7 @@ import { V1Transport } from "./transport.js";
|
|
|
67
87
|
/**
|
|
68
88
|
* One op's result: `id` and `version` for writes, `asserted` for asserts, and
|
|
69
89
|
* also `count` (decimal u64) for `assertCount`.
|
|
70
|
-
* @typedef {{ readonly id: string; readonly version: string } | { readonly asserted: true; readonly count?: string }} V1TransactionOpResult
|
|
90
|
+
* @typedef {{ readonly id: string; readonly version: string; readonly document?: import("./json.js").JsonObject } | { readonly asserted: true; readonly count?: string }} V1TransactionOpResult
|
|
71
91
|
*/
|
|
72
92
|
/** @typedef {{ readonly gte?: number | undefined; readonly lte?: number | undefined }} V1CountBounds */
|
|
73
93
|
/**
|
|
@@ -94,13 +114,14 @@ function pointer(path) {
|
|
|
94
114
|
*/
|
|
95
115
|
function patchOp(op) {
|
|
96
116
|
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);
|
|
99
117
|
let allowed = ["op", "path", "value"];
|
|
100
118
|
switch (op.op) {
|
|
101
119
|
case "unset": allowed = ["op", "path"]; break;
|
|
102
120
|
case "inc": allowed = ["op", "path", "by"]; break;
|
|
103
121
|
}
|
|
122
|
+
const path = pointer(op.path);
|
|
123
|
+
if (["inc", "add", "remove"].includes(op.op) && path.split("/").length !== 2)
|
|
124
|
+
throw new TypeError("inc, add and remove require a top-level field.");
|
|
104
125
|
for (const key of Object.keys(op)) if (!allowed.includes(key)) throw new TypeError(`Unknown patch op member ${key}.`);
|
|
105
126
|
switch (op.op) {
|
|
106
127
|
case "set":
|
|
@@ -109,14 +130,30 @@ function patchOp(op) {
|
|
|
109
130
|
return { op: op.op, path: pointer(op.path), value: op.value };
|
|
110
131
|
case "unset":
|
|
111
132
|
return { op: "unset", path: pointer(op.path) };
|
|
133
|
+
case "add":
|
|
134
|
+
case "remove": {
|
|
135
|
+
const value = op.value;
|
|
136
|
+
if (value !== null && typeof value !== "boolean" && typeof value !== "string" &&
|
|
137
|
+
typeof value !== "number" && typeof value !== "bigint" && !(value instanceof ExactDecimal))
|
|
138
|
+
throw new TypeError("add and remove require a JSON scalar of at most 1 KiB.");
|
|
139
|
+
assertJsonValue(value, "set element");
|
|
140
|
+
if (new TextEncoder().encode(stringifyExactJson(value, "set element")).byteLength > 1024)
|
|
141
|
+
throw new TypeError("add and remove require a JSON scalar of at most 1 KiB.");
|
|
142
|
+
return { op: op.op, path, value };
|
|
143
|
+
}
|
|
112
144
|
case "inc": {
|
|
113
145
|
const by = op.by;
|
|
114
|
-
if (!(typeof by === "bigint" || by instanceof ExactDecimal || (typeof by === "number" && Number.
|
|
115
|
-
throw new TypeError("inc
|
|
116
|
-
|
|
146
|
+
if (!(typeof by === "bigint" || by instanceof ExactDecimal || (typeof by === "number" && Number.isSafeInteger(by))))
|
|
147
|
+
throw new TypeError("inc requires a non-zero signed 64-bit integer.");
|
|
148
|
+
const text = String(by);
|
|
149
|
+
if (!/^-?(?:0|[1-9]\d*)$/u.test(text)) throw new TypeError("inc requires an integer JSON literal.");
|
|
150
|
+
const integer = BigInt(text);
|
|
151
|
+
if (integer === 0n || integer < -(1n << 63n) || integer >= (1n << 63n))
|
|
152
|
+
throw new TypeError("inc requires a non-zero signed 64-bit integer.");
|
|
153
|
+
return { op: "inc", path, by };
|
|
117
154
|
}
|
|
118
155
|
default:
|
|
119
|
-
throw new TypeError("Patch ops are set, unset, inc or
|
|
156
|
+
throw new TypeError("Patch ops are set, unset, inc, append, add or remove.");
|
|
120
157
|
}
|
|
121
158
|
}
|
|
122
159
|
|
|
@@ -135,7 +172,6 @@ function normalizePatch(patch) {
|
|
|
135
172
|
if (hasMerge) {
|
|
136
173
|
const merge = /** @type {{ readonly merge: JsonInputObject }} */ (patch).merge;
|
|
137
174
|
assertJsonObject(merge, "merge patch");
|
|
138
|
-
assertNoPayloadId(merge);
|
|
139
175
|
return { merge };
|
|
140
176
|
}
|
|
141
177
|
const ops = /** @type {{ readonly ops: readonly V1PatchOp[] }} */ (patch).ops;
|
|
@@ -144,6 +180,26 @@ function normalizePatch(patch) {
|
|
|
144
180
|
return { ops: ops.map(patchOp) };
|
|
145
181
|
}
|
|
146
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @param {V1NearSortKey} key
|
|
185
|
+
* @param {V1Query} query
|
|
186
|
+
* @returns {Record<string, unknown>}
|
|
187
|
+
*/
|
|
188
|
+
function nearSort(key, query) {
|
|
189
|
+
if (Object.keys(key).length !== 1 || query.sort?.length !== 1) throw new TypeError("near is the only sort key and has no path or order beside it.");
|
|
190
|
+
if (query.cursor != null) throw new TypeError("A near query has one page and takes no cursor.");
|
|
191
|
+
if (query.limit !== undefined && query.limit > 100) throw new TypeError("A near query limit is at most 100.");
|
|
192
|
+
const near = key.near;
|
|
193
|
+
if (near === null || typeof near !== "object") throw new TypeError("near is an object.");
|
|
194
|
+
for (const member of Object.keys(near)) if (!["path", "vector", "metric"].includes(member)) throw new TypeError(`Unknown near member ${member}.`);
|
|
195
|
+
if (near.metric !== undefined && near.metric !== "cosine" && near.metric !== "l2") throw new TypeError('near metric is "cosine" or "l2".');
|
|
196
|
+
const vector = near.vector;
|
|
197
|
+
if (!Array.isArray(vector) || vector.length < 1 || vector.length > 4096) throw new TypeError("A near vector has 1-4096 numbers.");
|
|
198
|
+
if (!vector.every(value => typeof value === "number" && Math.abs(value) <= 3.4028234663852886e38)) throw new TypeError("near vector values are finite float32 numbers.");
|
|
199
|
+
if (near.metric !== "l2" && vector.every(value => value === 0)) throw new TypeError("A cosine near vector is not all zeros.");
|
|
200
|
+
return { near: { path: requirePath(near.path), vector: [...vector], ...(near.metric === undefined ? {} : { metric: near.metric }) } };
|
|
201
|
+
}
|
|
202
|
+
|
|
147
203
|
/**
|
|
148
204
|
* @param {V1Query} query
|
|
149
205
|
* @returns {string}
|
|
@@ -159,6 +215,7 @@ function encodeQuery(query) {
|
|
|
159
215
|
if (!Array.isArray(query.sort) || query.sort.length > 4) throw new TypeError("sort has at most 4 keys.");
|
|
160
216
|
body.sort = query.sort.map((key) => {
|
|
161
217
|
if (key === null || typeof key !== "object") throw new TypeError("Sort keys are objects.");
|
|
218
|
+
if ("near" in key) return nearSort(key, query);
|
|
162
219
|
for (const member of Object.keys(key)) if (member !== "path" && member !== "order") throw new TypeError(`Unknown sort member ${member}.`);
|
|
163
220
|
if (key.order !== undefined && key.order !== "asc" && key.order !== "desc") throw new TypeError('Sort order is "asc" or "desc".');
|
|
164
221
|
return { path: requirePath(key.path), ...(key.order === undefined ? {} : { order: key.order }) };
|
|
@@ -187,7 +244,7 @@ function encodeQuery(query) {
|
|
|
187
244
|
* @param {JsonValue | undefined} value
|
|
188
245
|
* @returns {V1QueryPage}
|
|
189
246
|
*/
|
|
190
|
-
function decodeQueryPage(value) {
|
|
247
|
+
export function decodeQueryPage(value) {
|
|
191
248
|
const raw = object(value, "query response");
|
|
192
249
|
const next = raw.next_cursor;
|
|
193
250
|
if (next !== null && (typeof next !== "string" || next === "")) malformed("next_cursor");
|
|
@@ -198,7 +255,13 @@ function decodeQueryPage(value) {
|
|
|
198
255
|
count = Object.freeze({ value: u64(rawCount.value, "count value"), exact: boolean(rawCount.exact, "count exact flag") });
|
|
199
256
|
}
|
|
200
257
|
return Object.freeze({
|
|
201
|
-
items: Object.freeze(array(raw.items, "query items").map(
|
|
258
|
+
items: Object.freeze(array(raw.items, "query items").map(item => {
|
|
259
|
+
const distance = object(item, "document").distance;
|
|
260
|
+
if (distance === undefined) return decodeDocument(item);
|
|
261
|
+
const value = distance instanceof ExactDecimal ? distance.toNumber() : distance;
|
|
262
|
+
if (typeof value !== "number" || !Number.isFinite(value)) malformed("near distance");
|
|
263
|
+
return Object.freeze({ ...decodeDocument(item), distance: value });
|
|
264
|
+
})),
|
|
202
265
|
nextCursor: typeof next === "string" && next !== "" ? next : null,
|
|
203
266
|
count,
|
|
204
267
|
});
|
|
@@ -231,18 +294,11 @@ function rejectIdempotency(options) {
|
|
|
231
294
|
/** @param {JsonInputObject} document @returns {string} */
|
|
232
295
|
function encodeDocument(document) {
|
|
233
296
|
assertJsonObject(document, "document");
|
|
234
|
-
assertNoPayloadId(document);
|
|
235
297
|
const body = stringifyExactJson(document, "document");
|
|
236
298
|
if (new TextEncoder().encode(body).byteLength > 1024 * 1024) throw new TypeError("A document is at most 1 MiB.");
|
|
237
299
|
return body;
|
|
238
300
|
}
|
|
239
301
|
|
|
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
|
-
|
|
246
302
|
/** A collection handle by name. It performs no I/O until an operation runs. */
|
|
247
303
|
export class V1Collection {
|
|
248
304
|
/** @type {V1Transport} */
|
|
@@ -362,6 +418,38 @@ export class V1Collection {
|
|
|
362
418
|
return decodeQueryPage(response.value);
|
|
363
419
|
}
|
|
364
420
|
|
|
421
|
+
/** @param {{ readonly limit?: number | undefined; readonly cursor?: string | undefined }} [request] @param {V1RequestOptions} [options] @returns {Promise<V1QueryPage>} */
|
|
422
|
+
async list(request = {}, options = {}) {
|
|
423
|
+
for (const key of Object.keys(request)) if (key !== "limit" && key !== "cursor") throw new TypeError(`Unknown document list member ${key}.`);
|
|
424
|
+
const query = new URLSearchParams();
|
|
425
|
+
if (request.limit !== undefined) {
|
|
426
|
+
if (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 1000) throw new TypeError("Document list limit is 1-1000.");
|
|
427
|
+
query.set("limit", String(request.limit));
|
|
428
|
+
}
|
|
429
|
+
if (request.cursor !== undefined) {
|
|
430
|
+
if (typeof request.cursor !== "string" || request.cursor === "") throw new TypeError("Document cursor is a non-empty string.");
|
|
431
|
+
query.set("cursor", request.cursor);
|
|
432
|
+
}
|
|
433
|
+
const response = await this.#transport.call("listDocuments", this.#params, { query, signal: options.signal });
|
|
434
|
+
return decodeQueryPage(response.value);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @param {V1RequestOptions} [options] */
|
|
438
|
+
getSchema(options = {}) { return getCollectionSchema(this.#transport, this.#params, options); }
|
|
439
|
+
/** @param {import("./json.js").JsonInputObject | boolean} schema @param {V1RequestOptions} [options] */
|
|
440
|
+
putSchema(schema, options = {}) { return putCollectionSchema(this.#transport, this.#params, schema, options); }
|
|
441
|
+
/** @param {V1RequestOptions} [options] */
|
|
442
|
+
deleteSchema(options = {}) { return deleteCollectionSchema(this.#transport, this.#params, options); }
|
|
443
|
+
/** @param {V1RequestOptions} [options] */
|
|
444
|
+
getTtl(options = {}) { return getCollectionTtl(this.#transport, this.#params, options); }
|
|
445
|
+
/**
|
|
446
|
+
* Documents whose member at `path` holds a past RFC 3339 UTC time are deleted by an hourly sweep (up to about an hour late).
|
|
447
|
+
* @param {string} path JSON Pointer such as `/expiresAt`. @param {V1RequestOptions} [options]
|
|
448
|
+
*/
|
|
449
|
+
putTtl(path, options = {}) { return putCollectionTtl(this.#transport, this.#params, path, options); }
|
|
450
|
+
/** @param {V1RequestOptions} [options] */
|
|
451
|
+
deleteTtl(options = {}) { return deleteCollectionTtl(this.#transport, this.#params, options); }
|
|
452
|
+
|
|
365
453
|
/**
|
|
366
454
|
* Iterates over every page, following `nextCursor`. `count`
|
|
367
455
|
* is sent with the first page only.
|
|
@@ -372,9 +460,13 @@ export class V1Collection {
|
|
|
372
460
|
async *pages(query = {}, options = {}) {
|
|
373
461
|
/** @type {V1Query} */
|
|
374
462
|
let current = query;
|
|
463
|
+
const cursors = new Set(query.cursor == null ? [] : [query.cursor]);
|
|
375
464
|
while (true) {
|
|
376
465
|
const page = await this.query(current, options);
|
|
466
|
+
if (page.nextCursor !== null && cursors.has(page.nextCursor)) malformed("repeated pagination cursor");
|
|
467
|
+
if (page.nextCursor !== null) cursors.add(page.nextCursor);
|
|
377
468
|
yield page;
|
|
469
|
+
options.signal?.throwIfAborted();
|
|
378
470
|
if (page.nextCursor === null) return;
|
|
379
471
|
const { count: _count, ...rest } = current;
|
|
380
472
|
current = { ...rest, cursor: page.nextCursor };
|
|
@@ -388,17 +480,51 @@ export class V1Collection {
|
|
|
388
480
|
* @returns {AsyncGenerator<V1Document, void, undefined>}
|
|
389
481
|
*/
|
|
390
482
|
async *items(query = {}, options = {}) {
|
|
391
|
-
for await (const page of this.pages(query, options))
|
|
483
|
+
for await (const page of this.pages(query, options)) {
|
|
484
|
+
for (const item of page.items) {
|
|
485
|
+
options.signal?.throwIfAborted();
|
|
486
|
+
yield item;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
392
489
|
}
|
|
393
490
|
/**
|
|
394
491
|
* Reads one C# shape snapshot page or change-log chunk. Bootstrap defaults to offset -1.
|
|
395
492
|
* Echo handle, nextOffset and cursor on the following read; discard state on must_refetch.
|
|
493
|
+
* ifNoneMatch sends a complete entity-tag list or *; 304 throws not_modified with cache headers.
|
|
396
494
|
* @param {V1ShapeRequest} [request]
|
|
397
|
-
* @param {V1RequestOptions} [options]
|
|
495
|
+
* @param {V1RequestOptions & {readonly ifNoneMatch?: string}} [options]
|
|
398
496
|
* @returns {Promise<V1ShapePage>}
|
|
399
497
|
*/
|
|
400
498
|
shape(request = {}, options = {}) {
|
|
401
|
-
return
|
|
499
|
+
return readShapePage(this.#transport, this.#params, request, options.signal, options.ifNoneMatch);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Subscribe to the free-read v1 protocol. Stop with close() or the request signal.
|
|
504
|
+
* @param {{where?: V1Filter | undefined}} [request]
|
|
505
|
+
* @param {import("./shape-subscription.js").V1ShapeSubscribeOptions} [options]
|
|
506
|
+
* @returns {V1ShapeSubscription}
|
|
507
|
+
*/
|
|
508
|
+
subscribeShape(request = {}, options = {}) {
|
|
509
|
+
return new V1ShapeSubscription(this.#transport, this.#params, request, options);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** @param {import("./shape-manifest.js").V1ShapeManifestRequest} request @param {V1RequestOptions} [options] */
|
|
513
|
+
shapeManifest(request, options = {}) { return readShapeManifest(this.#transport, this.#params, request, options.signal); }
|
|
514
|
+
/** @param {import("./shape-reader.js").V1ShapeKeyRequest} request @param {V1RequestOptions} [options] */
|
|
515
|
+
shapeKey(request, options = {}) { return readShapeKey(this.#transport, this.#params, request, options.signal); }
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Verify a complete TBS1 object using its authenticated epoch key. Applies no state.
|
|
520
|
+
* Object delivery through manifests is pending the producer's FR-22 contract.
|
|
521
|
+
* @param {Uint8Array} bytes
|
|
522
|
+
* @param {import("./shape-reader.js").V1SealedShapePosition} position
|
|
523
|
+
* @param {V1RequestOptions} [options]
|
|
524
|
+
* @returns {Promise<V1ShapePage>}
|
|
525
|
+
*/
|
|
526
|
+
decryptShapeObject(bytes, position, options = {}) {
|
|
527
|
+
return decryptSealedShapePage(this.#transport, this.#params, bytes, position, options.signal);
|
|
402
528
|
}
|
|
403
529
|
|
|
404
530
|
}
|
|
@@ -457,7 +583,7 @@ export class V1Transaction {
|
|
|
457
583
|
static #precondition(precondition, allowNoneMatch) {
|
|
458
584
|
if (precondition === undefined) return {};
|
|
459
585
|
if ("ifMatch" in precondition && !("ifNoneMatchAny" in precondition))
|
|
460
|
-
return { if_match: precondition.ifMatch === "*" ? "*" : requireVersion(precondition.ifMatch) };
|
|
586
|
+
return { if_match: precondition.ifMatch === "*" ? "*" : `"${requireVersion(precondition.ifMatch)}"` };
|
|
461
587
|
if ("ifNoneMatchAny" in precondition && precondition.ifNoneMatchAny === true && !("ifMatch" in precondition)) {
|
|
462
588
|
if (!allowNoneMatch) throw new TypeError("This op does not take ifNoneMatchAny.");
|
|
463
589
|
return { if_none_match: "*" };
|
|
@@ -597,19 +723,21 @@ export class V1Transaction {
|
|
|
597
723
|
return Object.freeze(result.count === undefined
|
|
598
724
|
? { asserted: /** @type {const} */ (true) }
|
|
599
725
|
: { asserted: /** @type {const} */ (true), count: u64(result.count, "assert_count count") });
|
|
600
|
-
return Object.freeze({ id: requireDocumentId(result.id), version: requireVersion(result.version)
|
|
726
|
+
return Object.freeze({ id: requireDocumentId(result.id), version: requireVersion(result.version),
|
|
727
|
+
...(result.document === undefined ? {} : { document: documentObject(result.document) }),
|
|
728
|
+
});
|
|
601
729
|
})),
|
|
602
730
|
});
|
|
603
731
|
}
|
|
604
732
|
}
|
|
605
733
|
|
|
606
|
-
/** @typedef {{ readonly name: string; readonly createdAt: string }} V1CollectionInfo */
|
|
734
|
+
/** @typedef {{ readonly id: import("../ids.js").EntityId; readonly name: string; readonly createdAt: string }} V1CollectionInfo */
|
|
607
735
|
/** @typedef {{ readonly items: readonly V1CollectionInfo[]; readonly nextCursor: string | null }} V1CollectionPage */
|
|
608
736
|
/** @typedef {{ readonly limit?: number | undefined; readonly cursor?: string | undefined }} V1CollectionListOptions */
|
|
609
737
|
/** @param {JsonValue | undefined} value @returns {V1CollectionInfo} */
|
|
610
|
-
function decodeCollection(value) {
|
|
738
|
+
export function decodeCollection(value) {
|
|
611
739
|
const raw = object(value, "collection");
|
|
612
|
-
return Object.freeze({ name: requireName("collection", raw.name), createdAt: u64(raw.created_at, "collection created_at") });
|
|
740
|
+
return Object.freeze({ id: requireId(raw.id), name: requireName("collection", raw.name), createdAt: u64(raw.created_at, "collection created_at") });
|
|
613
741
|
}
|
|
614
742
|
/** Collection management on the public client listener. */
|
|
615
743
|
export class V1Collections {
|
|
@@ -663,6 +791,16 @@ export class V1Collections {
|
|
|
663
791
|
|
|
664
792
|
/** A database handle by name. It performs no I/O until an operation runs. */
|
|
665
793
|
export class V1Database {
|
|
794
|
+
/** @type {V1Imports} */
|
|
795
|
+
imports;
|
|
796
|
+
/** @type {V1Backups} */
|
|
797
|
+
backups;
|
|
798
|
+
/** @type {V1DatabaseUsageClient} */
|
|
799
|
+
metrics;
|
|
800
|
+
/** @type {V1Indexes} */
|
|
801
|
+
indexes;
|
|
802
|
+
/** @type {V1DatabaseSchemaClient} */
|
|
803
|
+
schema;
|
|
666
804
|
/** @type {V1Collections} */
|
|
667
805
|
collections;
|
|
668
806
|
/** @type {V1Transport} */
|
|
@@ -678,6 +816,11 @@ export class V1Database {
|
|
|
678
816
|
this.#transport = transport;
|
|
679
817
|
this.name = requireName("database", name);
|
|
680
818
|
this.collections = new V1Collections(transport, this.name);
|
|
819
|
+
this.schema = new V1DatabaseSchemaClient(transport, this.name);
|
|
820
|
+
this.indexes = new V1Indexes(transport, this.name);
|
|
821
|
+
this.metrics = new V1DatabaseUsageClient(transport, this.name);
|
|
822
|
+
this.backups = new V1Backups(transport, this.name);
|
|
823
|
+
this.imports = new V1Imports(transport, this.name);
|
|
681
824
|
Object.freeze(this);
|
|
682
825
|
}
|
|
683
826
|
|
|
@@ -694,7 +837,33 @@ export class V1Database {
|
|
|
694
837
|
return new V1Transaction(this.#transport, this.name);
|
|
695
838
|
}
|
|
696
839
|
|
|
840
|
+
/** @param {string} statement @param {V1RequestOptions} [options] */
|
|
841
|
+
sql(statement, options = {}) {
|
|
842
|
+
return runSql(this.#transport, this.name, statement, options);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** @param {string} statement @param {V1RequestOptions} [options] */
|
|
846
|
+
checkSql(statement, options = {}) {
|
|
847
|
+
return checkSql(this.#transport, this.name, statement, options);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** @param {string} statement @param {V1RequestOptions} [options] */
|
|
851
|
+
explainSql(statement, options = {}) {
|
|
852
|
+
return checkSql(this.#transport, this.name, statement, options, "explainSql");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** @param {import("./changes.js").V1ChangeStreamRequest} [request] @param {V1RequestOptions} [options] */
|
|
856
|
+
changes(request = {}, options = {}) {
|
|
857
|
+
return streamChanges(this.#transport, this.name, request, options.signal);
|
|
858
|
+
}
|
|
697
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Streams every live document as `{ collection, id, document }`, in collection then id order, from one snapshot.
|
|
862
|
+
* @param {import("./export.js").V1ExportOptions} [options]
|
|
863
|
+
*/
|
|
864
|
+
export(options = {}) {
|
|
865
|
+
return exportDocuments(this.#transport, this.name, options);
|
|
866
|
+
}
|
|
698
867
|
}
|
|
699
868
|
|
|
700
869
|
/**
|
|
@@ -702,12 +871,31 @@ export class V1Database {
|
|
|
702
871
|
* privately; it is never serialized, logged or forwarded to redirects.
|
|
703
872
|
*/
|
|
704
873
|
export class TerraBaseV1Client {
|
|
874
|
+
/** @type {V1Health} */
|
|
875
|
+
#health;
|
|
876
|
+
/** @type {V1Platform} */
|
|
877
|
+
#platform;
|
|
878
|
+
/** @type {V1BackupDestinationClient} */
|
|
879
|
+
#backupDestination;
|
|
880
|
+
/** @type {V1ProjectUsageClient} */
|
|
881
|
+
#metrics;
|
|
882
|
+
/** @type {V1SavedQueries} */
|
|
883
|
+
#savedQueries;
|
|
884
|
+
/** @type {V1Databases} */
|
|
885
|
+
#databases;
|
|
705
886
|
/** @type {V1Transport} */
|
|
706
887
|
#transport;
|
|
707
888
|
|
|
708
889
|
/** @param {TerraBaseV1ClientConfig} config */
|
|
709
890
|
constructor(config) {
|
|
891
|
+
requireId(config.project);
|
|
710
892
|
this.#transport = new V1Transport(config);
|
|
893
|
+
this.#databases = new V1Databases(this.#transport);
|
|
894
|
+
this.#savedQueries = new V1SavedQueries(this.#transport);
|
|
895
|
+
this.#metrics = new V1ProjectUsageClient(this.#transport);
|
|
896
|
+
this.#backupDestination = new V1BackupDestinationClient(this.#transport);
|
|
897
|
+
this.#health = new V1Health(this.#transport);
|
|
898
|
+
this.#platform = new V1Platform(this.#transport);
|
|
711
899
|
Object.freeze(this);
|
|
712
900
|
}
|
|
713
901
|
|
|
@@ -718,10 +906,23 @@ export class TerraBaseV1Client {
|
|
|
718
906
|
database(name) {
|
|
719
907
|
return new V1Database(this.#transport, name);
|
|
720
908
|
}
|
|
909
|
+
|
|
910
|
+
/** @returns {V1Databases} Project-scoped database catalog. */
|
|
911
|
+
get databases() { return this.#databases; }
|
|
912
|
+
/** @returns {V1SavedQueries} */
|
|
913
|
+
get savedQueries() { return this.#savedQueries; }
|
|
914
|
+
/** @returns {V1ProjectUsageClient} */
|
|
915
|
+
get metrics() { return this.#metrics; }
|
|
916
|
+
/** @returns {V1BackupDestinationClient} */
|
|
917
|
+
get backupDestination() { return this.#backupDestination; }
|
|
918
|
+
/** @returns {V1Health} */
|
|
919
|
+
get health() { return this.#health; }
|
|
920
|
+
/** @returns {V1Platform} */
|
|
921
|
+
get platform() { return this.#platform; }
|
|
721
922
|
}
|
|
722
923
|
|
|
723
924
|
/**
|
|
724
|
-
* Creates a TerraBase
|
|
925
|
+
* Creates a project-scoped TerraBase client for documents, SQL, changes and catalog operations.
|
|
725
926
|
* @param {TerraBaseV1ClientConfig} config
|
|
726
927
|
* @returns {TerraBaseV1Client}
|
|
727
928
|
*/
|