@terrascale/sdk 0.0.4 → 0.0.6

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.
@@ -0,0 +1,132 @@
1
+ import { array, malformed, object, string } from "./decode.js";
2
+ import { requireName } from "./ids.js";
3
+ import { ExactDecimal, stringifyExactJson } from "./json.js";
4
+ /**
5
+ * @import { JsonValue } from "./json.js"
6
+ * @import { V1RequestOptions } from "./terrabase-v1.js"
7
+ * @import { V1Transport } from "./transport.js"
8
+ */
9
+
10
+ /**
11
+ * An argument: null, a finite number, an int64 `bigint` or ExactDecimal, a boolean (stored as 1 or 0) or a string.
12
+ * @typedef {null | boolean | number | bigint | ExactDecimal | string} V1SqlArgument
13
+ */
14
+ /** @typedef {readonly V1SqlArgument[] | Readonly<Record<string, V1SqlArgument>>} V1SqlArguments Positional array, or named values keyed without their `:`, `@` or `$` prefix. */
15
+ /**
16
+ * @typedef {object} V1SqlStatement
17
+ * @property {string} sql
18
+ * @property {readonly V1SqlArgument[] | undefined} [args] Positional arguments.
19
+ * @property {Readonly<Record<string, V1SqlArgument>> | undefined} [named] Named arguments, keys without their prefix.
20
+ */
21
+ /**
22
+ * A result value. Integers beyond 2^53 decode as ExactDecimal, keeping every digit; `BigInt(String(value))` converts one.
23
+ * @typedef {null | number | ExactDecimal | string} V1SqlValue
24
+ */
25
+ /** @typedef {{ readonly name: string; readonly decltype: string | null }} V1SqlColumn A column; `decltype` is null for an expression. */
26
+ /**
27
+ * @typedef {object} V1SqlExecuteResult
28
+ * @property {readonly V1SqlColumn[]} columns
29
+ * @property {readonly (readonly V1SqlValue[])[]} rows One value per column.
30
+ * @property {number} rowsAffected
31
+ * @property {number | ExactDecimal | null} lastInsertRowid
32
+ */
33
+
34
+ const maximumStatements = 256;
35
+ const maximumSqlLength = 16_384;
36
+
37
+ /** @param {unknown} value @param {string} where */
38
+ function requireArgument(value, where) {
39
+ if (value === null || typeof value === "boolean" || typeof value === "string" || value instanceof ExactDecimal) return;
40
+ if (typeof value === "number" && Number.isFinite(value)) return;
41
+ if (typeof value === "bigint" && BigInt.asIntN(64, value) === value) return;
42
+ throw new TypeError(`SQL ${where} is null, a finite number, an int64 bigint, a boolean or a string.`);
43
+ }
44
+
45
+ /** @param {unknown} statement @param {number} index */
46
+ function encodeStatement(statement, index) {
47
+ if (typeof statement !== "string" && (typeof statement !== "object" || statement === null || Array.isArray(statement)))
48
+ throw new TypeError(`SQL statement ${index} is a string or { sql, args, named }.`);
49
+ const { sql, args, named, ...extra } = /** @type {V1SqlStatement} */ (typeof statement === "string" ? { sql: statement } : statement);
50
+ for (const member of Object.keys(extra)) throw new TypeError(`Unknown SQL statement member ${member}.`);
51
+ if (typeof sql !== "string" || sql.length === 0 || sql.length > maximumSqlLength)
52
+ throw new TypeError(`SQL statement ${index} is a non-empty string of at most ${maximumSqlLength} UTF-16 code units.`);
53
+ /** @type {{ sql: string; args?: V1SqlArgument[]; named?: Record<string, V1SqlArgument> }} */
54
+ const encoded = { sql };
55
+ if (args !== undefined) {
56
+ if (!Array.isArray(args)) throw new TypeError(`SQL statement ${index} args is an array.`);
57
+ args.forEach((arg, position) => requireArgument(arg, `argument ${position}`));
58
+ encoded.args = [...args];
59
+ }
60
+ if (named !== undefined) {
61
+ if (named === null || typeof named !== "object" || Array.isArray(named)) throw new TypeError(`SQL statement ${index} named is an object.`);
62
+ for (const [name, value] of Object.entries(named)) {
63
+ if (/^[:@$]/u.test(name)) throw new TypeError(`SQL named argument ${name} is keyed without its prefix.`);
64
+ requireArgument(value, `named argument ${name}`);
65
+ }
66
+ encoded.named = { ...named };
67
+ }
68
+ return encoded;
69
+ }
70
+
71
+ /** @param {string} sql @param {V1SqlArguments | undefined} args @returns {V1SqlStatement} */
72
+ export function sqlStatement(sql, args) {
73
+ if (args === undefined) return { sql };
74
+ return Array.isArray(args) ? { sql, args } : { sql, named: /** @type {Readonly<Record<string, V1SqlArgument>>} */ (args) };
75
+ }
76
+
77
+ /** @param {JsonValue | undefined} value @returns {V1SqlValue} */
78
+ function decodeValue(value) {
79
+ if (value === null || typeof value === "number" || typeof value === "string" || value instanceof ExactDecimal) return value;
80
+ return malformed("SQL value");
81
+ }
82
+
83
+ /** @param {JsonValue | undefined} value @returns {V1SqlExecuteResult} */
84
+ function decodeResult(value) {
85
+ const raw = object(value, "SQL statement result");
86
+ const columns = array(raw.columns, "SQL columns").map(entry => {
87
+ const column = object(entry, "SQL column");
88
+ const decltype = column.decltype === null || column.decltype === undefined ? null : string(column.decltype, "SQL column decltype");
89
+ return Object.freeze({ name: string(column.name, "SQL column name"), decltype });
90
+ });
91
+ const rows = array(raw.rows, "SQL rows").map(entry => {
92
+ const row = array(entry, "SQL row").map(decodeValue);
93
+ if (row.length !== columns.length) malformed("SQL row width");
94
+ return Object.freeze(row);
95
+ });
96
+ const affected = raw.rows_affected;
97
+ if (typeof affected !== "number" || !Number.isSafeInteger(affected) || affected < 0) malformed("SQL rows_affected");
98
+ const rowid = raw.last_insert_rowid ?? null;
99
+ if (rowid !== null && !(rowid instanceof ExactDecimal) && !(typeof rowid === "number" && Number.isInteger(rowid))) malformed("SQL last_insert_rowid");
100
+ return Object.freeze({ columns: Object.freeze(columns), rows: Object.freeze(rows), rowsAffected: affected, lastInsertRowid: rowid });
101
+ }
102
+
103
+ /**
104
+ * Runs 1 to 256 statements as one transaction: all commit or none do. BEGIN, COMMIT and ROLLBACK are refused.
105
+ * A refused statement throws SqlError with its index.
106
+ * @param {V1Transport} transport
107
+ * @param {string} database
108
+ * @param {readonly (string | V1SqlStatement)[]} statements
109
+ * @param {V1RequestOptions} [options]
110
+ * @returns {Promise<readonly V1SqlExecuteResult[]>}
111
+ */
112
+ export async function executeSql(transport, database, statements, options = {}) {
113
+ if (!Array.isArray(statements) || statements.length === 0 || statements.length > maximumStatements)
114
+ throw new TypeError(`SQL batches hold 1 to ${maximumStatements} statements.`);
115
+ if (options === null || typeof options !== "object" || Array.isArray(options)) throw new TypeError("SQL request options must be an object.");
116
+ for (const member of Object.keys(options)) if (member !== "signal") throw new TypeError(`Unknown SQL request option ${member}.`);
117
+ const body = stringifyExactJson({ statements: statements.map(encodeStatement) }, "SQL statements");
118
+ const response = await transport.call("executeSql", { database: requireName("database", database) }, { body, signal: options.signal });
119
+ const results = array(object(response.value, "SQL execute response").results, "SQL results").map(decodeResult);
120
+ if (results.length !== statements.length) malformed("SQL result count");
121
+ return Object.freeze(results);
122
+ }
123
+
124
+ /**
125
+ * Rows as objects keyed by column name; a later duplicate column name wins.
126
+ * @template [T=Record<string, V1SqlValue>]
127
+ * @param {V1SqlExecuteResult} result
128
+ * @returns {T[]}
129
+ */
130
+ export function sqlRowObjects(result) {
131
+ return result.rows.map(row => /** @type {T} */ (Object.fromEntries(result.columns.map((column, index) => [column.name, row[index] ?? null]))));
132
+ }
@@ -11,8 +11,10 @@ import { readShapeManifest } from "./shape-manifest.js";
11
11
  import { V1ShapeSubscription } from "./shape-subscription.js";
12
12
  import { decryptSealedShapePage, readShapePage, readShapeKey } from "./shape-reader.js";
13
13
  import { runSql, checkSql } from "./sql.js";
14
+ import { executeSql, sqlRowObjects, sqlStatement } from "./sql-execute.js";
14
15
  import { streamChanges } from "./changes.js";
15
- import { V1Databases, getCollectionSchema, putCollectionSchema, deleteCollectionSchema } from "./catalog.js";
16
+ import { exportDocuments } from "./export.js";
17
+ import { V1Databases, getCollectionSchema, putCollectionSchema, deleteCollectionSchema, getCollectionTtl, putCollectionTtl, deleteCollectionTtl } from "./catalog.js";
16
18
  import { V1Transport } from "./transport.js";
17
19
  import { V1Health, V1Platform } from "./system.js";
18
20
  import { V1Imports } from "./imports.js";
@@ -21,6 +23,7 @@ import { V1ProjectUsageClient, V1DatabaseUsageClient } from "./usage.js";
21
23
  import { V1Indexes } from "./indexes.js";
22
24
  import { V1DatabaseSchemaClient } from "./database-schema.js";
23
25
  import { V1SavedQueries } from "./saved-queries.js";
26
+ import { V1ChangeWebhooks } from "./change-webhooks.js";
24
27
  /**
25
28
  * @import { V1Document, V1Durability, V1WriteResult } from "./decode.js"
26
29
  * @import { V1Filter, V1Path } from "./filter.js"
@@ -58,12 +61,17 @@ import { V1SavedQueries } from "./saved-queries.js";
58
61
  * @typedef {{ readonly merge: JsonInputObject } | { readonly ops: readonly V1PatchOp[] }} V1Patch
59
62
  */
60
63
 
61
- /** @typedef {{ readonly path: V1Path; readonly order?: "asc" | "desc" | undefined }} V1SortKey */
64
+ /** @typedef {{ readonly path: V1Path; readonly order?: "asc" | "desc" | undefined }} V1FieldSortKey */
65
+ /**
66
+ * Vector similarity sort, nearest first. It must be the only sort key; limit is at most 100 and there is no cursor.
67
+ * @typedef {{ readonly near: { readonly path: V1Path; readonly vector: readonly number[]; readonly metric?: "cosine" | "l2" | undefined } }} V1NearSortKey
68
+ */
69
+ /** @typedef {V1FieldSortKey | V1NearSortKey} V1SortKey */
62
70
  /**
63
71
  * A query-v1 request. Every member is optional.
64
72
  * @typedef {object} V1Query
65
73
  * @property {V1Filter | null | undefined} [filter]
66
- * @property {readonly V1SortKey[] | undefined} [sort] At most 4 keys; results are finally ordered by `$id`.
74
+ * @property {readonly V1SortKey[] | undefined} [sort] At most 4 keys, or one `near` key; results are finally ordered by `$id`.
67
75
  * @property {readonly string[] | undefined} [project] At most 64 paths.
68
76
  * @property {number | undefined} [limit] 1-1000 (server default 50).
69
77
  * @property {string | null | undefined} [cursor] A previous page's `nextCursor`.
@@ -174,6 +182,26 @@ function normalizePatch(patch) {
174
182
  return { ops: ops.map(patchOp) };
175
183
  }
176
184
 
185
+ /**
186
+ * @param {V1NearSortKey} key
187
+ * @param {V1Query} query
188
+ * @returns {Record<string, unknown>}
189
+ */
190
+ function nearSort(key, query) {
191
+ 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.");
192
+ if (query.cursor != null) throw new TypeError("A near query has one page and takes no cursor.");
193
+ if (query.limit !== undefined && query.limit > 100) throw new TypeError("A near query limit is at most 100.");
194
+ const near = key.near;
195
+ if (near === null || typeof near !== "object") throw new TypeError("near is an object.");
196
+ for (const member of Object.keys(near)) if (!["path", "vector", "metric"].includes(member)) throw new TypeError(`Unknown near member ${member}.`);
197
+ if (near.metric !== undefined && near.metric !== "cosine" && near.metric !== "l2") throw new TypeError('near metric is "cosine" or "l2".');
198
+ const vector = near.vector;
199
+ if (!Array.isArray(vector) || vector.length < 1 || vector.length > 4096) throw new TypeError("A near vector has 1-4096 numbers.");
200
+ if (!vector.every(value => typeof value === "number" && Math.abs(value) <= 3.4028234663852886e38)) throw new TypeError("near vector values are finite float32 numbers.");
201
+ if (near.metric !== "l2" && vector.every(value => value === 0)) throw new TypeError("A cosine near vector is not all zeros.");
202
+ return { near: { path: requirePath(near.path), vector: [...vector], ...(near.metric === undefined ? {} : { metric: near.metric }) } };
203
+ }
204
+
177
205
  /**
178
206
  * @param {V1Query} query
179
207
  * @returns {string}
@@ -189,6 +217,7 @@ function encodeQuery(query) {
189
217
  if (!Array.isArray(query.sort) || query.sort.length > 4) throw new TypeError("sort has at most 4 keys.");
190
218
  body.sort = query.sort.map((key) => {
191
219
  if (key === null || typeof key !== "object") throw new TypeError("Sort keys are objects.");
220
+ if ("near" in key) return nearSort(key, query);
192
221
  for (const member of Object.keys(key)) if (member !== "path" && member !== "order") throw new TypeError(`Unknown sort member ${member}.`);
193
222
  if (key.order !== undefined && key.order !== "asc" && key.order !== "desc") throw new TypeError('Sort order is "asc" or "desc".');
194
223
  return { path: requirePath(key.path), ...(key.order === undefined ? {} : { order: key.order }) };
@@ -228,7 +257,13 @@ export function decodeQueryPage(value) {
228
257
  count = Object.freeze({ value: u64(rawCount.value, "count value"), exact: boolean(rawCount.exact, "count exact flag") });
229
258
  }
230
259
  return Object.freeze({
231
- items: Object.freeze(array(raw.items, "query items").map(decodeDocument)),
260
+ items: Object.freeze(array(raw.items, "query items").map(item => {
261
+ const distance = object(item, "document").distance;
262
+ if (distance === undefined) return decodeDocument(item);
263
+ const value = distance instanceof ExactDecimal ? distance.toNumber() : distance;
264
+ if (typeof value !== "number" || !Number.isFinite(value)) malformed("near distance");
265
+ return Object.freeze({ ...decodeDocument(item), distance: value });
266
+ })),
232
267
  nextCursor: typeof next === "string" && next !== "" ? next : null,
233
268
  count,
234
269
  });
@@ -268,6 +303,8 @@ function encodeDocument(document) {
268
303
 
269
304
  /** A collection handle by name. It performs no I/O until an operation runs. */
270
305
  export class V1Collection {
306
+ /** @type {V1ChangeWebhooks} */
307
+ webhooks;
271
308
  /** @type {V1Transport} */
272
309
  #transport;
273
310
  /** @type {Readonly<Record<string, string>>} */
@@ -286,6 +323,7 @@ export class V1Collection {
286
323
  this.#transport = transport;
287
324
  this.database = requireName("database", database);
288
325
  this.name = requireName("collection", name);
326
+ this.webhooks = new V1ChangeWebhooks(transport, this.database, this.name);
289
327
  this.#params = Object.freeze({ database: this.database, collection: this.name });
290
328
  Object.freeze(this);
291
329
  }
@@ -407,6 +445,15 @@ export class V1Collection {
407
445
  putSchema(schema, options = {}) { return putCollectionSchema(this.#transport, this.#params, schema, options); }
408
446
  /** @param {V1RequestOptions} [options] */
409
447
  deleteSchema(options = {}) { return deleteCollectionSchema(this.#transport, this.#params, options); }
448
+ /** @param {V1RequestOptions} [options] */
449
+ getTtl(options = {}) { return getCollectionTtl(this.#transport, this.#params, options); }
450
+ /**
451
+ * Documents whose member at `path` holds a past RFC 3339 UTC time are deleted by an hourly sweep (up to about an hour late).
452
+ * @param {string} path JSON Pointer such as `/expiresAt`. @param {V1RequestOptions} [options]
453
+ */
454
+ putTtl(path, options = {}) { return putCollectionTtl(this.#transport, this.#params, path, options); }
455
+ /** @param {V1RequestOptions} [options] */
456
+ deleteTtl(options = {}) { return deleteCollectionTtl(this.#transport, this.#params, options); }
410
457
 
411
458
  /**
412
459
  * Iterates over every page, following `nextCursor`. `count`
@@ -810,10 +857,51 @@ export class V1Database {
810
857
  return checkSql(this.#transport, this.name, statement, options, "explainSql");
811
858
  }
812
859
 
860
+ /**
861
+ * Runs one relational SQL statement as its own transaction. Reads need documents:read; writes and DDL need documents:write.
862
+ * @param {string} sql
863
+ * @param {import("./sql-execute.js").V1SqlArguments} [args] Positional array, or named values keyed without their prefix.
864
+ * @param {V1RequestOptions} [options]
865
+ * @returns {Promise<import("./sql-execute.js").V1SqlExecuteResult>}
866
+ */
867
+ async execute(sql, args, options = {}) {
868
+ const [result] = await executeSql(this.#transport, this.name, [sqlStatement(sql, args)], options);
869
+ return /** @type {import("./sql-execute.js").V1SqlExecuteResult} */ (result);
870
+ }
871
+
872
+ /**
873
+ * Runs one statement and returns its rows as objects keyed by column name.
874
+ * @template [T=Record<string, import("./sql-execute.js").V1SqlValue>]
875
+ * @param {string} sql
876
+ * @param {import("./sql-execute.js").V1SqlArguments} [args]
877
+ * @param {V1RequestOptions} [options]
878
+ * @returns {Promise<T[]>}
879
+ */
880
+ async query(sql, args, options = {}) {
881
+ return sqlRowObjects(await this.execute(sql, args, options));
882
+ }
883
+
884
+ /**
885
+ * Runs 1 to 256 statements as one transaction: all commit or none do. A refused statement throws SqlError.
886
+ * @param {readonly (string | import("./sql-execute.js").V1SqlStatement)[]} statements
887
+ * @param {V1RequestOptions} [options]
888
+ */
889
+ batch(statements, options = {}) {
890
+ return executeSql(this.#transport, this.name, statements, options);
891
+ }
892
+
813
893
  /** @param {import("./changes.js").V1ChangeStreamRequest} [request] @param {V1RequestOptions} [options] */
814
894
  changes(request = {}, options = {}) {
815
895
  return streamChanges(this.#transport, this.name, request, options.signal);
816
896
  }
897
+
898
+ /**
899
+ * Streams every live document as `{ collection, id, document }`, in collection then id order, from one snapshot.
900
+ * @param {import("./export.js").V1ExportOptions} [options]
901
+ */
902
+ export(options = {}) {
903
+ return exportDocuments(this.#transport, this.name, options);
904
+ }
817
905
  }
818
906
 
819
907
  /**
@@ -23,12 +23,13 @@ const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-
23
23
  * @typedef {{ readonly project: string | undefined; readonly database?: string | undefined; readonly signal?: AbortSignal | undefined }} V1EndpointContext
24
24
  */
25
25
  /** @typedef {(context: V1EndpointContext) => string | Promise<string>} V1EndpointResolver */
26
+ /** A fresh access token for each authenticated request, including stream reconnects. @typedef {() => string | Promise<string>} V1TokenProvider */
26
27
  /**
27
28
  * Project-scoped C# Documents API client. Supply exactly one of baseUrl or endpointResolver.
28
29
  * URLs must use HTTPS or loopback HTTP. Mutations are never automatically replayed.
29
30
  * @typedef {object} TerraBaseV1ClientConfig
30
31
  * @property {string} project
31
- * @property {string} token A ts-auth project access JWT (or a legacy C# tb_ key); held privately.
32
+ * @property {string | V1TokenProvider} token An access JWT (or a legacy C# tb_ key), or a provider resolved per authenticated request; held privately. Browser callers must use end-user tokens, never project credentials.
32
33
  * @property {string | undefined} [baseUrl]
33
34
  * @property {V1EndpointResolver | undefined} [endpointResolver]
34
35
  * @property {FetchLike | undefined} [fetch]
@@ -117,6 +118,13 @@ function canonicalBaseUrl(raw) {
117
118
  return `${url.origin}${url.pathname.replace(/\/+$/u, "")}`;
118
119
  }
119
120
 
121
+ /** @param {unknown} token @returns {string} */
122
+ function requireToken(token) {
123
+ if (typeof token !== "string" || token.trim() !== token || !tokenPattern.test(token))
124
+ throw new TypeError("token must be a compact access JWT or a legacy C# tb_ API key.");
125
+ return token;
126
+ }
127
+
120
128
  /** A response body that failed in transit after its headers arrived. */
121
129
  export class BodyTransportError extends Error {
122
130
  /** @param {unknown} cause */
@@ -134,7 +142,7 @@ export class V1Transport {
134
142
  #resolver;
135
143
  /** @type {Map<string, { promise: Promise<string>; timer: ReturnType<typeof setTimeout>; waiters: number }>} */
136
144
  #bases = new Map();
137
- /** @type {string | undefined} */
145
+ /** @type {string | V1TokenProvider | undefined} */
138
146
  #token;
139
147
  /** @type {FetchLike} */
140
148
  #fetch;
@@ -150,8 +158,8 @@ export class V1Transport {
150
158
  if (config.endpointResolver !== undefined && typeof config.endpointResolver !== "function")
151
159
  throw new TypeError("endpointResolver must be a function.");
152
160
  this.#resolver = config.endpointResolver ?? staticBaseUrlResolver(/** @type {string} */ (config.baseUrl));
153
- if ((config.project !== undefined || config.token !== undefined) && (typeof config.token !== "string" || !tokenPattern.test(config.token)))
154
- throw new TypeError("token must be a compact access JWT or a legacy C# tb_ API key.");
161
+ if ((config.project !== undefined || config.token !== undefined) && typeof config.token !== "function")
162
+ requireToken(config.token);
155
163
  if ("autoIdempotencyKey" in config || "maxRetries" in config || "resolver" in config)
156
164
  throw new TypeError("Unsupported client configuration member.");
157
165
  const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
@@ -164,6 +172,35 @@ export class V1Transport {
164
172
  this.#maxResponseBytes = maxResponseBytes;
165
173
  }
166
174
 
175
+ /** Internal credential lifetime metadata; never exposes the bearer value. */
176
+ get hasTokenProvider() { return typeof this.#token === "function"; }
177
+
178
+ /** @param {AbortSignal | undefined} signal @returns {Promise<string>} */
179
+ async #accessToken(signal) {
180
+ signal?.throwIfAborted();
181
+ const provider = this.#token;
182
+ if (typeof provider !== "function") return requireToken(provider);
183
+ /** @type {() => void} */
184
+ let abort = () => {};
185
+ try {
186
+ const token = await new Promise((resolve, reject) => {
187
+ abort = () => reject(signal?.reason);
188
+ signal?.addEventListener("abort", abort, { once: true });
189
+ void Promise.resolve().then(() => {
190
+ signal?.throwIfAborted();
191
+ return provider();
192
+ }).then(resolve, () => {
193
+ // Provider errors may contain credentials; do not retain their message or cause.
194
+ reject(new Error("TerraBase token provider failed."));
195
+ });
196
+ });
197
+ signal?.throwIfAborted();
198
+ return requireToken(token);
199
+ } finally {
200
+ signal?.removeEventListener("abort", abort);
201
+ }
202
+ }
203
+
167
204
  /**
168
205
  * @param {V1Operation} operation
169
206
  * @param {Readonly<Record<string, string>>} params
@@ -320,7 +357,7 @@ export class V1Transport {
320
357
  }
321
358
 
322
359
  /**
323
- * @param {"streamDatabaseChanges"} operation
360
+ * @param {"streamDatabaseChanges" | "exportDatabase"} operation
324
361
  * @param {Readonly<Record<string, string>>} params
325
362
  * @param {V1CallOptions} [options]
326
363
  * @returns {Promise<Response>}
@@ -328,9 +365,10 @@ export class V1Transport {
328
365
  async openStream(operation, params, options = {}) {
329
366
  const response = await this.#send(operation, params, options);
330
367
  const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
331
- if (mediaType !== "text/event-stream" || response.body === null) {
368
+ const expected = v1Route(operation).responseMediaType;
369
+ if (mediaType !== expected || response.body === null) {
332
370
  void response.body?.cancel().catch(() => undefined);
333
- throw new TypeError("TerraBase changes require a text/event-stream response body.");
371
+ throw new TypeError(`TerraBase ${operation} requires a ${expected} response body.`);
334
372
  }
335
373
  return response;
336
374
  }
@@ -344,7 +382,8 @@ export class V1Transport {
344
382
  let url = await this.url(operation, params, options.signal);
345
383
  if (options.query !== undefined && options.query.size > 0) url += `?${options.query}`;
346
384
  const headers = new Headers(options.headers);
347
- if (route.permission !== "anonymous" && this.#token !== undefined) headers.set("authorization", `Bearer ${this.#token}`);
385
+ if (route.permission !== "anonymous" && this.#token !== undefined)
386
+ headers.set("authorization", `Bearer ${await this.#accessToken(options.signal)}`);
348
387
  if (route.responseMediaType !== "") headers.set("accept", route.responseMediaType);
349
388
  if (options.body !== undefined) headers.set("content-type", options.contentType ?? "application/json");
350
389
  /** @type {RequestInit & { duplex?: "half" }} */
@@ -357,6 +396,7 @@ export class V1Transport {
357
396
  init.duplex = "half";
358
397
  } else if (options.body !== undefined) init.body = typeof options.body === "string" ? options.body : options.body.slice().buffer;
359
398
  if (options.signal !== undefined) init.signal = options.signal;
399
+ options.signal?.throwIfAborted();
360
400
  const response = await this.#fetch(url, init);
361
401
  if (options.signal?.aborted) {
362
402
  void response.body?.cancel().catch(() => undefined);
package/types/index.d.ts CHANGED
@@ -6,12 +6,14 @@ export { V1Databases } from "./v1/catalog.js";
6
6
  export { V1SavedQueries } from "./v1/saved-queries.js";
7
7
  export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
8
8
  export { V1Indexes } from "./v1/indexes.js";
9
+ export { V1ChangeWebhooks } from "./v1/change-webhooks.js";
9
10
  export { V1Imports } from "./v1/imports.js";
10
11
  export type V1ShapeSubscribeOptions = import("./v1/shape-subscription.js").V1ShapeSubscribeOptions;
11
12
  export type V1ShapeDocument = import("./v1/shape-subscription.js").V1ShapeDocument;
12
13
  export type V1SealedShapePosition = import("./v1/shape-reader.js").V1SealedShapePosition;
13
14
  export type EntityId = import("./ids.js").EntityId;
14
15
  export type TerraBaseV1ClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
16
+ export type V1TokenProvider = import("./v1/transport.js").V1TokenProvider;
15
17
  export type V1RequestOptions = import("./v1/terrabase-v1.js").V1RequestOptions;
16
18
  export type V1ReadOptions = import("./v1/terrabase-v1.js").V1ReadOptions;
17
19
  export type V1WriteOptions = import("./v1/terrabase-v1.js").V1WriteOptions;
@@ -20,6 +22,7 @@ export type V1ConditionalWriteOptions = import("./v1/terrabase-v1.js").V1Conditi
20
22
  export type V1PatchOp = import("./v1/terrabase-v1.js").V1PatchOp;
21
23
  export type V1Patch = import("./v1/terrabase-v1.js").V1Patch;
22
24
  export type V1SortKey = import("./v1/terrabase-v1.js").V1SortKey;
25
+ export type V1NearSortKey = import("./v1/terrabase-v1.js").V1NearSortKey;
23
26
  export type V1Query = import("./v1/terrabase-v1.js").V1Query;
24
27
  export type V1Count = import("./v1/terrabase-v1.js").V1Count;
25
28
  export type V1QueryPage = import("./v1/terrabase-v1.js").V1QueryPage;
@@ -54,9 +57,17 @@ export type V1SqlRow = import("./v1/sql.js").V1SqlRow;
54
57
  export type V1SqlResult = import("./v1/sql.js").V1SqlResult;
55
58
  export type V1SqlIndexSuggestion = import("./v1/sql.js").V1SqlIndexSuggestion;
56
59
  export type V1SqlCheck = import("./v1/sql.js").V1SqlCheck;
60
+ export type V1SqlArgument = import("./v1/sql-execute.js").V1SqlArgument;
61
+ export type V1SqlArguments = import("./v1/sql-execute.js").V1SqlArguments;
62
+ export type V1SqlStatement = import("./v1/sql-execute.js").V1SqlStatement;
63
+ export type V1SqlValue = import("./v1/sql-execute.js").V1SqlValue;
64
+ export type V1SqlColumn = import("./v1/sql-execute.js").V1SqlColumn;
65
+ export type V1SqlExecuteResult = import("./v1/sql-execute.js").V1SqlExecuteResult;
57
66
  export type V1ChangeStreamRequest = import("./v1/changes.js").V1ChangeStreamRequest;
58
67
  export type V1DatabaseChange = import("./v1/changes.js").V1DatabaseChange;
59
68
  export type V1DatabaseChangeCursor = import("./v1/changes.js").V1DatabaseChangeCursor;
69
+ export type V1ExportOptions = import("./v1/export.js").V1ExportOptions;
70
+ export type V1ExportedDocument = import("./v1/export.js").V1ExportedDocument;
60
71
  export type V1ManagedDatabase = import("./v1/catalog.js").V1ManagedDatabase;
61
72
  export type V1DatabaseCreateRequest = import("./v1/catalog.js").V1DatabaseCreateRequest;
62
73
  export type V1DatabaseUpdateRequest = import("./v1/catalog.js").V1DatabaseUpdateRequest;
@@ -64,6 +75,7 @@ export type V1DatabaseBranchRequest = import("./v1/catalog.js").V1DatabaseBranch
64
75
  export type V1DatabaseListRequest = import("./v1/catalog.js").V1DatabaseListRequest;
65
76
  export type V1DatabasePage = import("./v1/catalog.js").V1DatabasePage;
66
77
  export type V1CollectionSchema = import("./v1/catalog.js").V1CollectionSchema;
78
+ export type V1CollectionTtl = import("./v1/catalog.js").V1CollectionTtl;
67
79
  export type V1InferredSchema = import("./v1/database-schema.js").V1InferredSchema;
68
80
  export type V1DatabaseSchema = import("./v1/database-schema.js").V1DatabaseSchema;
69
81
  export type V1UniqueSchema = import("./v1/database-schema.js").V1UniqueSchema;
@@ -81,6 +93,10 @@ export type V1Analytics = import("./v1/usage.js").V1Analytics;
81
93
  export type V1Replication = import("./v1/usage.js").V1Replication;
82
94
  export type V1UsageRequest = import("./v1/usage.js").V1UsageRequest;
83
95
  export type V1AnalyticsRequest = import("./v1/usage.js").V1AnalyticsRequest;
96
+ export type V1ChangeWebhook = import("./v1/change-webhooks.js").V1ChangeWebhook;
97
+ export type V1CreatedChangeWebhook = import("./v1/change-webhooks.js").V1CreatedChangeWebhook;
98
+ export type V1ChangeWebhookDelivery = import("./v1/change-webhooks.js").V1ChangeWebhookDelivery;
99
+ export type V1ChangeWebhookListRequest = import("./v1/change-webhooks.js").V1ChangeWebhookListRequest;
84
100
  export type V1Backup = import("./v1/backups.js").V1Backup;
85
101
  export type V1BackupPage = import("./v1/backups.js").V1BackupPage;
86
102
  export type V1BackupPolicy = import("./v1/backups.js").V1BackupPolicy;
@@ -105,7 +121,7 @@ export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest
105
121
  export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
106
122
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
107
123
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
108
- export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
124
+ export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, SqlError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
109
125
  export { generateId, isValidId, requireId } from "./ids.js";
110
126
  export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
111
127
  export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
@@ -4,6 +4,12 @@ export function getCollectionSchema(transport: V1Transport, params: Readonly<Rec
4
4
  export function putCollectionSchema(transport: V1Transport, params: Readonly<Record<string, string>>, schema: boolean | JsonInputObject, options?: V1RequestOptions): Promise<V1CollectionSchema>;
5
5
  /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionSchema>} */
6
6
  export function deleteCollectionSchema(transport: V1Transport, params: Readonly<Record<string, string>>, options?: V1RequestOptions): Promise<V1CollectionSchema>;
7
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionTtl>} */
8
+ export function getCollectionTtl(transport: V1Transport, params: Readonly<Record<string, string>>, options?: V1RequestOptions): Promise<V1CollectionTtl>;
9
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {string} path @param {V1RequestOptions} [options] @returns {Promise<V1CollectionTtl>} */
10
+ export function putCollectionTtl(transport: V1Transport, params: Readonly<Record<string, string>>, path: string, options?: V1RequestOptions): Promise<V1CollectionTtl>;
11
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionTtl>} */
12
+ export function deleteCollectionTtl(transport: V1Transport, params: Readonly<Record<string, string>>, options?: V1RequestOptions): Promise<V1CollectionTtl>;
7
13
  /** Public database catalog operations, bound to the transport's authorized project. */
8
14
  export class V1Databases {
9
15
  /** @param {V1Transport} transport */
@@ -86,6 +92,12 @@ export type V1CollectionSchema = {
86
92
  readonly schema: JsonObject | boolean | null;
87
93
  readonly dialect?: string | null | undefined;
88
94
  };
95
+ /**
96
+ * The expiry member, or null when documents never expire.
97
+ */
98
+ export type V1CollectionTtl = {
99
+ readonly path: string | null;
100
+ };
89
101
  import type { V1Transport } from "./transport.js";
90
102
  import type { V1RequestOptions } from "./terrabase-v1.js";
91
103
  import type { JsonInputObject } from "./json.js";
@@ -0,0 +1,59 @@
1
+ /** Collection change webhook registration and sanitized delivery history. Requests are never replayed. */
2
+ export class V1ChangeWebhooks {
3
+ /** @param {V1Transport} transport @param {string} database @param {string} collection */
4
+ constructor(transport: V1Transport, database: string, collection: string);
5
+ /**
6
+ * Registers from the current feed head. Requires documents:read and documents:write.
7
+ * Store secret immediately; it is non-enumerable to keep it out of JSON and ordinary diagnostics.
8
+ * @param {{ readonly url: string }} request @param {V1RequestOptions} [options] @returns {Promise<V1CreatedChangeWebhook>}
9
+ */
10
+ create(request: {
11
+ readonly url: string;
12
+ }, options?: V1RequestOptions): Promise<V1CreatedChangeWebhook>;
13
+ /**
14
+ * Requires documents:read. Ascending IDs; continue with the final ID until an empty array. No secrets.
15
+ * @param {V1ChangeWebhookListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<readonly V1ChangeWebhook[]>}
16
+ */
17
+ list(request?: V1ChangeWebhookListRequest, options?: V1RequestOptions): Promise<readonly V1ChangeWebhook[]>;
18
+ /**
19
+ * Requires documents:read and documents:write. An in-flight delivery may still complete. Unknown ID returns 404.
20
+ * @param {string} id @param {V1RequestOptions} [options] @returns {Promise<void>}
21
+ */
22
+ delete(id: string, options?: V1RequestOptions): Promise<void>;
23
+ /**
24
+ * Requires documents:read. Latest 100 retained records; paginate with the final ID until an empty array.
25
+ * @param {string} id @param {V1ChangeWebhookListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<readonly V1ChangeWebhookDelivery[]>}
26
+ */
27
+ deliveries(id: string, request?: V1ChangeWebhookListRequest, options?: V1RequestOptions): Promise<readonly V1ChangeWebhookDelivery[]>;
28
+ #private;
29
+ }
30
+ export type V1ChangeWebhook = ReturnType<typeof webhook>;
31
+ export type V1CreatedChangeWebhook = V1ChangeWebhook & {
32
+ readonly secret: string;
33
+ };
34
+ export type V1ChangeWebhookDelivery = ReturnType<typeof deliveries>[number];
35
+ export type V1ChangeWebhookListRequest = {
36
+ readonly after?: string;
37
+ readonly limit?: number;
38
+ };
39
+ import type { V1RequestOptions } from "./terrabase-v1.js";
40
+ import type { V1Transport } from "./transport.js";
41
+ /** @import { V1Transport } from "./transport.js" */
42
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
43
+ declare const webhook: import("./management-decode.js").Decoder<Readonly<{
44
+ id: string;
45
+ url: string;
46
+ createdAt: string;
47
+ status: string;
48
+ }>>;
49
+ declare const deliveries: import("./management-decode.js").Decoder<readonly Readonly<{
50
+ id: string;
51
+ createdAt: string;
52
+ status: string;
53
+ attempts: number | import("./json.js").ExactDecimal;
54
+ lastAttemptAt: string | null;
55
+ nextAttemptAt: string | null;
56
+ httpStatus: number | import("./json.js").ExactDecimal | null;
57
+ errorCode: string | null;
58
+ }>[]>;
59
+ export {};
@@ -9,6 +9,7 @@
9
9
  * @property {"value" | "conflict" | (string & {})} state
10
10
  * @property {JsonObject} document
11
11
  * @property {readonly JsonValue[]} [siblings] Live versions and tombstones on a causal conflict.
12
+ * @property {number} [distance] Distance to the query vector; present only on `near` query results.
12
13
  */
13
14
  /**
14
15
  * The result of a document write.
@@ -97,6 +98,10 @@ export type V1Document = {
97
98
  * Live versions and tombstones on a causal conflict.
98
99
  */
99
100
  siblings?: readonly JsonValue[];
101
+ /**
102
+ * Distance to the query vector; present only on `near` query results.
103
+ */
104
+ distance?: number;
100
105
  };
101
106
  /**
102
107
  * The result of a document write.
@@ -92,6 +92,9 @@ export const V1ErrorCode: Readonly<{
92
92
  internalError: "internal_error";
93
93
  capacity: "capacity";
94
94
  notModified: "not_modified";
95
+ sqlError: "sql_error";
96
+ sqlConflict: "sql_conflict";
97
+ sqlTooLarge: "sql_too_large";
95
98
  }>;
96
99
  /** `412 precondition_failed`: an If-Match, If-None-Match, assert or assert_count failed. */
97
100
  export class V1PreconditionFailedError extends TerraBaseV1Error {
@@ -105,6 +108,21 @@ export class V1ForbiddenScopeError extends TerraBaseV1Error {
105
108
  /** `404 not_found`: absent or deleted; `etag` carries a tombstone version. */
106
109
  export class V1NotFoundError extends TerraBaseV1Error {
107
110
  }
111
+ /**
112
+ * A refused relational SQL statement: `sql_error` (400), `forbidden_scope` (403, writes and DDL need
113
+ * documents:write), `sql_conflict` (409) or `sql_too_large` (413). The whole request rolled back.
114
+ */
115
+ export class SqlError extends TerraBaseV1Error {
116
+ /** @param {TerraBaseV1ErrorInit & { statement: number; sqliteCode?: string | undefined }} init */
117
+ constructor(init: TerraBaseV1ErrorInit & {
118
+ statement: number;
119
+ sqliteCode?: string | undefined;
120
+ });
121
+ /** @type {number} Zero-based index of the failing statement. */
122
+ statement: number;
123
+ /** @type {string | undefined} The SQLite error code, such as `SQLITE_CONSTRAINT_PRIMARYKEY`. */
124
+ sqliteCode: string | undefined;
125
+ }
108
126
  export type TerraBaseV1ErrorInit = {
109
127
  status: number;
110
128
  code: string;