@terrascale/sdk 0.0.4 → 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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased — collection TTL and vector near sort
4
+
5
+ `collection.putTtl(path)`, `getTtl()` and `deleteTtl()` call
6
+ `PUT/GET/DELETE .../collections/{collection}/ttl` and return `{ path }`
7
+ (`null` when unset). `query({ sort: [{ near: { path, vector, metric } }] })`
8
+ sorts by vector distance and each item carries `distance`.
9
+
10
+ ## Unreleased — database export
11
+
12
+ `db.export({ after, signal })` streams TerraBase's
13
+ `GET /v1/projects/{project}/databases/{database}/export` NDJSON as an async
14
+ iterable of `{ collection, id, document }`. `after: { collection, id }` resumes
15
+ after that document. Error answers raise the usual `TerraBaseV1Error`.
16
+
3
17
  ## 0.0.4
4
18
 
5
19
  Publishes current main, including `db.sql`, `db.changes`, backups, indexes and
package/README.md CHANGED
@@ -108,9 +108,11 @@ OpenAPI and immutable release qualification remain open gates.
108
108
  | --- | --- |
109
109
  | Documents | `collection.get/put/create/patch/delete/list/query/pages/items` |
110
110
  | Collections and declared JSON Schema | `db.collections.create/get/list/drop`; `collection.getSchema/putSchema/deleteSchema` |
111
+ | Collection TTL | `collection.putTtl/getTtl/deleteTtl` |
111
112
  | Databases | `client.databases.create/list/get/update/delete/branch` |
112
113
  | SQL | `db.sql/checkSql/explainSql` |
113
114
  | Changes | `db.changes` (SSE async iterator) |
115
+ | Export | `db.export` (NDJSON async iterator) |
114
116
 
115
117
  Project, collection catalog, and database catalog IDs are canonical uppercase
116
118
  26-character ULIDs. `generateId()`, `isValidId()` and `requireId()` retain that
@@ -136,6 +138,21 @@ does not support `lower` paths or query-body `explain`; query number comparisons
136
138
  64-bit integer/double arithmetic, even when the SDK preserves exact JSON numbers.
137
139
  `ExactDecimal` and bigint serialization require Node.js 22+ or a current browser.
138
140
 
141
+ A `near` sort orders by vector distance, nearest first, and adds `distance` to
142
+ each item. It is the only sort key, `metric` is `cosine` (default) or `l2`,
143
+ `limit` is at most 100 and there is one page (no cursor):
144
+
145
+ ```js
146
+ await users.putTtl("/expiresAt"); // getTtl() -> { path: "/expiresAt" }; deleteTtl() clears
147
+ const { items } = await docs.query({
148
+ sort: [{ near: { path: "/embedding", vector: [0.1, 0.2], metric: "cosine" } }], limit: 10,
149
+ });
150
+ ```
151
+
152
+ A collection TTL deletes documents whose member holds a past RFC 3339 UTC time.
153
+ An hourly home-region sweep does the delete, up to about an hour late; until
154
+ then expired documents stay readable.
155
+
139
156
  `db.transaction()` builds one bounded atomic group across collections in that
140
157
  database. It supports document operations, `assert`, `assertAbsent` and
141
158
  `assertCount`; assertions examine the state before the group's writes.
@@ -169,6 +186,18 @@ errors, denials, expired history (410) and overload (429) propagate. The SDK
169
186
  never silently resets to `now` or broadens the view. Iterator `return()` also
170
187
  interrupts an already pending read.
171
188
 
189
+ `db.export()` streams every live document as `{ collection, id, document }`,
190
+ in collection then id order, from one read snapshot (`documents:read`). Lines
191
+ are parsed as they arrive. A stream that ends inside a line throws; resume with
192
+ the last whole document received, which reads a new snapshot:
193
+
194
+ ```js
195
+ let last;
196
+ for await (const row of db.export({ after: last, signal: controller.signal })) {
197
+ last = { collection: row.collection, id: row.id };
198
+ }
199
+ ```
200
+
172
201
  The exported collection `shape()` remains compatible with the historical
173
202
  `6cb9745…` release. Its route is absent from the audited current server source;
174
203
  do not use it as current producer evidence. SSE changes are a separate database
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrascale/sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "JavaScript SDK, typed with JSDoc, for project-scoped C# TerraBase v1 and hosted TerraScale identity.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -45,6 +45,10 @@
45
45
  {"method":"GET","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/shape","operation":"getShape","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200,307],"responseMediaType":"application/json","source":"src/TerraBase/Features/Shapes/GetShapeEndpoint.cs","request":"none"},
46
46
  {"method":"GET","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/shape/keys","operation":"getShapeKey","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Shapes/GetShapeKeyEndpoint.cs","request":"none"},
47
47
  {"method":"GET","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/shape/manifest","operation":"getShapeManifest","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Shapes/GetShapeManifestEndpoint.cs","request":"none"},
48
+ {"method":"DELETE","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/ttl","operation":"deleteCollectionTtl","scopes":["documents:write"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Documents/DeleteCollectionTtlEndpoint.cs","request":"none","producerChange":"TerraScale/terrascale#416"},
49
+ {"method":"GET","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/ttl","operation":"getCollectionTtl","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Documents/GetCollectionTtlEndpoint.cs","request":"none","producerChange":"TerraScale/terrascale#416"},
50
+ {"method":"PUT","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/ttl","operation":"putCollectionTtl","scopes":["documents:write"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Documents/PutCollectionTtlEndpoint.cs","request":"CollectionTtlBody","producerChange":"TerraScale/terrascale#416"},
51
+ {"method":"GET","path":"/v1/projects/{project}/databases/{database}/export","operation":"exportDatabase","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/x-ndjson","source":"src/TerraBase/Features/Documents/ExportDatabaseEndpoint.cs","request":"none","producerChange":"TerraScale/terrascale#412"},
48
52
  {"method":"GET","path":"/v1/projects/{project}/databases/{database}/import","operation":"getImport","scopes":["databases:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Import/GetImportEndpoint.cs","request":"none"},
49
53
  {"method":"PATCH","path":"/v1/projects/{project}/databases/{database}/import","operation":"updateImport","scopes":["databases:write","documents:write"],"requestMediaTypes":["application/json"],"statuses":[202],"responseMediaType":"application/json","source":"src/TerraBase/Features/Import/UpdateImportEndpoint.cs","request":"UpdateImportRequest"},
50
54
  {"method":"POST","path":"/v1/projects/{project}/databases/{database}/import","operation":"createImport","scopes":["databases:write","documents:write"],"requestMediaTypes":["application/json"],"statuses":[200,201],"responseMediaType":"application/json","source":"src/TerraBase/Features/Import/CreateImportEndpoint.cs","request":"CreateImportRequest"},
package/src/index.js CHANGED
@@ -28,6 +28,7 @@ export { generateId, isValidId, requireId } from "./ids.js";
28
28
  export { ExactDecimal } from "./v1/json.js";
29
29
 
30
30
  /** @typedef {import("./v1/transport.js").TerraBaseV1ClientConfig} TerraBaseV1ClientConfig */
31
+ /** @typedef {import("./v1/transport.js").V1TokenProvider} V1TokenProvider */
31
32
  /** @typedef {import("./v1/terrabase-v1.js").V1RequestOptions} V1RequestOptions */
32
33
  /** @typedef {import("./v1/terrabase-v1.js").V1ReadOptions} V1ReadOptions */
33
34
  /** @typedef {import("./v1/terrabase-v1.js").V1WriteOptions} V1WriteOptions */
@@ -36,6 +37,7 @@ export { ExactDecimal } from "./v1/json.js";
36
37
  /** @typedef {import("./v1/terrabase-v1.js").V1PatchOp} V1PatchOp */
37
38
  /** @typedef {import("./v1/terrabase-v1.js").V1Patch} V1Patch */
38
39
  /** @typedef {import("./v1/terrabase-v1.js").V1SortKey} V1SortKey */
40
+ /** @typedef {import("./v1/terrabase-v1.js").V1NearSortKey} V1NearSortKey */
39
41
  /** @typedef {import("./v1/terrabase-v1.js").V1Query} V1Query */
40
42
  /** @typedef {import("./v1/terrabase-v1.js").V1Count} V1Count */
41
43
  /** @typedef {import("./v1/terrabase-v1.js").V1QueryPage} V1QueryPage */
@@ -77,6 +79,8 @@ export { V1SavedQueries } from "./v1/saved-queries.js";
77
79
  /** @typedef {import("./v1/changes.js").V1ChangeStreamRequest} V1ChangeStreamRequest */
78
80
  /** @typedef {import("./v1/changes.js").V1DatabaseChange} V1DatabaseChange */
79
81
  /** @typedef {import("./v1/changes.js").V1DatabaseChangeCursor} V1DatabaseChangeCursor */
82
+ /** @typedef {import("./v1/export.js").V1ExportOptions} V1ExportOptions */
83
+ /** @typedef {import("./v1/export.js").V1ExportedDocument} V1ExportedDocument */
80
84
  /** @typedef {import("./v1/catalog.js").V1ManagedDatabase} V1ManagedDatabase */
81
85
  /** @typedef {import("./v1/catalog.js").V1DatabaseCreateRequest} V1DatabaseCreateRequest */
82
86
  /** @typedef {import("./v1/catalog.js").V1DatabaseUpdateRequest} V1DatabaseUpdateRequest */
@@ -84,6 +88,7 @@ export { V1SavedQueries } from "./v1/saved-queries.js";
84
88
  /** @typedef {import("./v1/catalog.js").V1DatabaseListRequest} V1DatabaseListRequest */
85
89
  /** @typedef {import("./v1/catalog.js").V1DatabasePage} V1DatabasePage */
86
90
  /** @typedef {import("./v1/catalog.js").V1CollectionSchema} V1CollectionSchema */
91
+ /** @typedef {import("./v1/catalog.js").V1CollectionTtl} V1CollectionTtl */
87
92
 
88
93
  export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
89
94
  /** @typedef {import("./v1/database-schema.js").V1InferredSchema} V1InferredSchema */
package/src/v1/catalog.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { requireId } from "../ids.js";
2
2
  import { array, malformed, object, string } from "./decode.js";
3
3
  import { requireName } from "./ids.js";
4
+ import { isJsonPointer } from "./filter.js";
4
5
  import { isJsonObject, stringifyExactJson } from "./json.js";
5
6
  /**
6
7
  * @import { JsonInputObject, JsonObject, JsonValue } from "./json.js"
@@ -26,6 +27,7 @@ import { isJsonObject, stringifyExactJson } from "./json.js";
26
27
  /** @typedef {{ readonly after?: string | undefined; readonly limit?: number | undefined }} V1DatabaseListRequest */
27
28
  /** @typedef {{ readonly items: readonly V1ManagedDatabase[]; readonly next: string | null }} V1DatabasePage */
28
29
  /** @typedef {{ readonly schema: JsonObject | boolean | null; readonly dialect?: string | null | undefined }} V1CollectionSchema */
30
+ /** The expiry member, or null when documents never expire. @typedef {{ readonly path: string | null }} V1CollectionTtl */
29
31
 
30
32
  /** @param {unknown} value @param {readonly string[]} allowed @returns {void} */
31
33
  function members(value, allowed) {
@@ -208,3 +210,31 @@ export async function deleteCollectionSchema(transport, params, options = {}) {
208
210
  mutationOptions(options);
209
211
  return collectionSchema((await transport.call("deleteCollectionSchema", params, { signal: options.signal })).value);
210
212
  }
213
+
214
+ /** @param {JsonValue | undefined} value @returns {V1CollectionTtl} */
215
+ function collectionTtl(value) {
216
+ const path = object(value, "collection TTL").path;
217
+ if (path !== null && typeof path !== "string") malformed("TTL path");
218
+ return Object.freeze({ path });
219
+ }
220
+
221
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionTtl>} */
222
+ export async function getCollectionTtl(transport, params, options = {}) {
223
+ return collectionTtl((await transport.call("getCollectionTtl", params, { signal: options.signal })).value);
224
+ }
225
+
226
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {string} path @param {V1RequestOptions} [options] @returns {Promise<V1CollectionTtl>} */
227
+ export async function putCollectionTtl(transport, params, path, options = {}) {
228
+ mutationOptions(options);
229
+ if (!isJsonPointer(path))
230
+ throw new TypeError("A TTL path is a JSON Pointer of 1 to 16 object members.");
231
+ return collectionTtl((await transport.call("putCollectionTtl", params, {
232
+ body: stringifyExactJson({ path }, "collection TTL"), signal: options.signal,
233
+ })).value);
234
+ }
235
+
236
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionTtl>} */
237
+ export async function deleteCollectionTtl(transport, params, options = {}) {
238
+ mutationOptions(options);
239
+ return collectionTtl((await transport.call("deleteCollectionTtl", params, { signal: options.signal })).value);
240
+ }
package/src/v1/decode.js CHANGED
@@ -13,6 +13,7 @@ import { isJsonObject } from "./json.js";
13
13
  * @property {"value" | "conflict" | (string & {})} state
14
14
  * @property {JsonObject} document
15
15
  * @property {readonly JsonValue[]} [siblings] Live versions and tombstones on a causal conflict.
16
+ * @property {number} [distance] Distance to the query vector; present only on `near` query results.
16
17
  */
17
18
 
18
19
  /**
@@ -0,0 +1,68 @@
1
+ import { documentObject, malformed, object } from "./decode.js";
2
+ import { requireDocumentId, requireName } from "./ids.js";
3
+ import { parseExactJson } from "./json.js";
4
+ import { BodyTransportError } from "./transport.js";
5
+ /** @import { JsonObject } from "./json.js" */
6
+ /** @import { V1Transport } from "./transport.js" */
7
+
8
+ /** @typedef {{ readonly collection: string; readonly id: string; readonly document: JsonObject }} V1ExportedDocument */
9
+ /**
10
+ * @typedef {object} V1ExportOptions
11
+ * @property {{ readonly collection: string; readonly id: string } | undefined} [after] Resume after this document; the resumed part reads a new snapshot.
12
+ * @property {AbortSignal | undefined} [signal]
13
+ */
14
+
15
+ // A 1 MiB document may require six characters per character when JSON escapes it.
16
+ const maximumLineLength = 8 * 1024 * 1024;
17
+
18
+ /** @param {string} line @returns {V1ExportedDocument} */
19
+ function decodeLine(line) {
20
+ /** @type {import("./json.js").JsonValue} */
21
+ let value;
22
+ try { value = parseExactJson(line); }
23
+ catch { malformed("export line JSON"); }
24
+ const raw = object(value, "export line");
25
+ return Object.freeze({ collection: requireName("collection", raw.collection), id: requireDocumentId(raw.id), document: documentObject(raw.document) });
26
+ }
27
+
28
+ /**
29
+ * Streams every live document of a database as NDJSON, in collection then id order, from one read snapshot.
30
+ * A stream that ends inside a line is incomplete and throws; resume with `after` set to the last document received.
31
+ * @param {V1Transport} transport
32
+ * @param {string} database
33
+ * @param {V1ExportOptions} [options]
34
+ * @returns {AsyncGenerator<V1ExportedDocument, void, undefined>}
35
+ */
36
+ export async function* exportDocuments(transport, database, options = {}) {
37
+ const { after, signal } = options;
38
+ const query = new URLSearchParams();
39
+ if (after !== undefined) {
40
+ if (typeof after !== "object" || after === null) throw new TypeError("export after is { collection, id }.");
41
+ query.set("after_collection", requireName("collection", after.collection));
42
+ query.set("after_id", requireDocumentId(after.id));
43
+ }
44
+ const response = await transport.openStream("exportDatabase", { database }, { query, signal });
45
+ const body = /** @type {ReadableStream<Uint8Array>} */ (response.body);
46
+ const decoder = /** @type {ReadableWritablePair<string, Uint8Array>} */ (new TextDecoderStream("utf-8", { fatal: true, ignoreBOM: true }));
47
+ const reader = body.pipeThrough(decoder).getReader();
48
+ let pending = "";
49
+ try {
50
+ while (true) {
51
+ /** @type {ReadableStreamReadResult<string>} */
52
+ let part;
53
+ try { part = await reader.read(); }
54
+ catch (error) { signal?.throwIfAborted(); throw new BodyTransportError(error); }
55
+ if (part.done) break;
56
+ // Split only the new text so a long line arriving in many chunks is scanned once.
57
+ const lines = part.value.split("\n");
58
+ lines[0] = pending + lines[0];
59
+ pending = /** @type {string} */ (lines.pop());
60
+ if (pending.length > maximumLineLength) malformed("export line exceeding 8 MiB");
61
+ for (const line of lines) yield decodeLine(line);
62
+ }
63
+ if (pending !== "") malformed("incomplete export line");
64
+ } finally {
65
+ await reader.cancel().catch(() => undefined);
66
+ reader.releaseLock();
67
+ }
68
+ }
@@ -1,8 +1,13 @@
1
- /** @typedef {"getShapeManifest" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery"} ManagementOperation */
1
+ /** @typedef {"getShapeManifest" | "exportDatabase" | "putCollectionTtl" | "getCollectionTtl" | "deleteCollectionTtl" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery"} ManagementOperation */
2
2
  const saved = "/v1/projects/{project}/saved-queries";
3
+ const ttl = "/v1/projects/{project}/databases/{database}/collections/{collection}/ttl";
3
4
  /** @type {readonly import("./routes.js").V1Route[]} */
4
5
  export const managementV1Routes = Object.freeze([
5
6
  route("getShapeManifest", "GET", "/v1/projects/{project}/databases/{database}/collections/{collection}/shape/manifest"),
7
+ route("exportDatabase", "GET", "/v1/projects/{project}/databases/{database}/export"),
8
+ route("putCollectionTtl", "PUT", ttl),
9
+ route("getCollectionTtl", "GET", ttl),
10
+ route("deleteCollectionTtl", "DELETE", ttl),
6
11
  route("healthz", "GET", "/healthz"),
7
12
  route("readyz", "GET", "/readyz"),
8
13
  route("backupz", "GET", "/backupz"),
@@ -58,10 +63,10 @@ function route(operation, method, pathTemplate, status = 200) {
58
63
  const write = method !== "GET" && operation !== "planPointInTime";
59
64
  const noBody = ["createBackup", "applyIndexRecommendation", "cancelImport", "resumeImport"].includes(operation);
60
65
  return Object.freeze({ operation, method, pathTemplate,
61
- permission: operation === "getShapeManifest" ? "documents:read" : pathTemplate.startsWith("/v1/platform/") ? "platform-admin" : !pathTemplate.startsWith("/v1/") ? "anonymous" : write ? "databases:write" : "databases:read",
66
+ permission: operation === "getShapeManifest" || operation === "exportDatabase" || operation === "getCollectionTtl" ? "documents:read" : pathTemplate === ttl ? "documents:write" : pathTemplate.startsWith("/v1/platform/") ? "platform-admin" : !pathTemplate.startsWith("/v1/") ? "anonymous" : write ? "databases:write" : "databases:read",
62
67
  additionalPermissions: Object.freeze(["createImport", "uploadImportSource", "updateImport", "startImport", "resumeImport"].includes(operation) ? ["documents:write"] : []),
63
68
  requestMediaTypes: Object.freeze(noBody ? [] : operation === "uploadImportSource" ? ["application/octet-stream"] : method === "POST" || method === "PUT" || method === "PATCH" ? ["application/json"] : []),
64
- responseMediaType: status === 204 ? "" : "application/json", successStatuses: Object.freeze(operation === "readyz" || operation === "backupz" ? [200, 503] : operation === "applyIndexRecommendation" || operation === "createImport" ? [200, 201] : [status]),
69
+ responseMediaType: status === 204 ? "" : operation === "exportDatabase" ? "application/x-ndjson" : "application/json", successStatuses: Object.freeze(operation === "readyz" || operation === "backupz" ? [200, 503] : operation === "applyIndexRecommendation" || operation === "createImport" ? [200, 201] : [status]),
65
70
  idempotencyKey: "none", replaySafe: !write, contract: "docs/terrabase/api.md", contractText: Object.freeze([]),
66
71
  });
67
72
  }
@@ -9,7 +9,7 @@ import { TerraBaseV1Error } from "./errors.js";
9
9
  /** @import { V1ShapeRequest, V1ShapePage } from "./shapes.js" */
10
10
  /** @typedef {{handle: string; offset: string; epoch: string; keyId: string}} V1SealedShapePosition */
11
11
  /** @typedef {{ latestEpoch: bigint; keys: Map<string, { epoch: bigint; key: Uint8Array }> }} ShapeKeys */
12
- /** @type {WeakMap<V1Transport, Map<string, ShapeKeys>>} Keys stay scoped to a credential, database and collection. */
12
+ /** @type {WeakMap<V1Transport, Map<string, ShapeKeys>>} Only immutable credentials retain keys across decrypts. */
13
13
  const shapeKeys = new WeakMap();
14
14
  /** @param {Readonly<Record<string, string>>} params */
15
15
  const keyScope = params => JSON.stringify([params.database, params.collection]);
@@ -26,8 +26,10 @@ async function shapeCall(transport, params, operation, options) {
26
26
  throw error;
27
27
  }
28
28
  }
29
- /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @returns {ShapeKeys} */
29
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @returns {ShapeKeys | undefined} */
30
30
  function currentKeys(transport, params) {
31
+ // Callback tokens can change users; authorize every decrypt without retaining their keys.
32
+ if (transport.hasTokenProvider) return undefined;
31
33
  let scopes = shapeKeys.get(transport);
32
34
  if (scopes === undefined) { scopes = new Map(); shapeKeys.set(transport, scopes); }
33
35
  const scope = keyScope(params);
@@ -152,7 +154,7 @@ export async function readShapePage(transport, params, request, signal, ifNoneMa
152
154
  return page;
153
155
  }
154
156
  /**
155
- * Reuse process-memory keys for this handle/epoch/KID; fetch misses without HTTP cache reuse.
157
+ * Reuse keys only for static credentials; callback tokens authorize every decrypt with no-store.
156
158
  * @param {V1Transport} transport @param {Readonly<Record<string,string>>} params
157
159
  * @param {Uint8Array} bytes @param {V1SealedShapePosition} position @param {AbortSignal | undefined} signal
158
160
  * @returns {Promise<V1ShapePage>}
@@ -167,7 +169,7 @@ export async function decryptSealedShapePage(transport, params, bytes, position,
167
169
  signal?.throwIfAborted();
168
170
  const cache = currentKeys(transport, params);
169
171
  const cacheId = JSON.stringify([handle, epoch, keyId]);
170
- let raw = cache.keys.get(cacheId)?.key;
172
+ let raw = cache?.keys.get(cacheId)?.key;
171
173
  if (raw === undefined) {
172
174
  const query = new URLSearchParams({ handle, epoch, key_id: keyId });
173
175
  const response = await shapeCall(transport, params, "getShapeKey", { query, cache: "no-store", signal });
@@ -179,7 +181,7 @@ export async function decryptSealedShapePage(transport, params, bytes, position,
179
181
  if (btoa(String.fromCharCode(...raw)).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "") !== encoded) malformed("shape epoch key encoding");
180
182
  const servedEpoch = BigInt(epoch);
181
183
  const active = currentKeys(transport, params);
182
- if (active === cache && servedEpoch >= cache.latestEpoch) {
184
+ if (cache !== undefined && active === cache && servedEpoch >= cache.latestEpoch) {
183
185
  cache.latestEpoch = servedEpoch;
184
186
  for (const [id, entry] of cache.keys) if (entry.epoch < servedEpoch) cache.keys.delete(id);
185
187
  cache.keys.set(cacheId, { epoch: servedEpoch, key: raw });
@@ -12,7 +12,8 @@ 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
14
  import { streamChanges } from "./changes.js";
15
- import { V1Databases, getCollectionSchema, putCollectionSchema, deleteCollectionSchema } from "./catalog.js";
15
+ import { exportDocuments } from "./export.js";
16
+ import { V1Databases, getCollectionSchema, putCollectionSchema, deleteCollectionSchema, getCollectionTtl, putCollectionTtl, deleteCollectionTtl } from "./catalog.js";
16
17
  import { V1Transport } from "./transport.js";
17
18
  import { V1Health, V1Platform } from "./system.js";
18
19
  import { V1Imports } from "./imports.js";
@@ -58,12 +59,17 @@ import { V1SavedQueries } from "./saved-queries.js";
58
59
  * @typedef {{ readonly merge: JsonInputObject } | { readonly ops: readonly V1PatchOp[] }} V1Patch
59
60
  */
60
61
 
61
- /** @typedef {{ readonly path: V1Path; readonly order?: "asc" | "desc" | undefined }} V1SortKey */
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 */
62
68
  /**
63
69
  * A query-v1 request. Every member is optional.
64
70
  * @typedef {object} V1Query
65
71
  * @property {V1Filter | null | undefined} [filter]
66
- * @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`.
67
73
  * @property {readonly string[] | undefined} [project] At most 64 paths.
68
74
  * @property {number | undefined} [limit] 1-1000 (server default 50).
69
75
  * @property {string | null | undefined} [cursor] A previous page's `nextCursor`.
@@ -174,6 +180,26 @@ function normalizePatch(patch) {
174
180
  return { ops: ops.map(patchOp) };
175
181
  }
176
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
+
177
203
  /**
178
204
  * @param {V1Query} query
179
205
  * @returns {string}
@@ -189,6 +215,7 @@ function encodeQuery(query) {
189
215
  if (!Array.isArray(query.sort) || query.sort.length > 4) throw new TypeError("sort has at most 4 keys.");
190
216
  body.sort = query.sort.map((key) => {
191
217
  if (key === null || typeof key !== "object") throw new TypeError("Sort keys are objects.");
218
+ if ("near" in key) return nearSort(key, query);
192
219
  for (const member of Object.keys(key)) if (member !== "path" && member !== "order") throw new TypeError(`Unknown sort member ${member}.`);
193
220
  if (key.order !== undefined && key.order !== "asc" && key.order !== "desc") throw new TypeError('Sort order is "asc" or "desc".');
194
221
  return { path: requirePath(key.path), ...(key.order === undefined ? {} : { order: key.order }) };
@@ -228,7 +255,13 @@ export function decodeQueryPage(value) {
228
255
  count = Object.freeze({ value: u64(rawCount.value, "count value"), exact: boolean(rawCount.exact, "count exact flag") });
229
256
  }
230
257
  return Object.freeze({
231
- items: Object.freeze(array(raw.items, "query items").map(decodeDocument)),
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
+ })),
232
265
  nextCursor: typeof next === "string" && next !== "" ? next : null,
233
266
  count,
234
267
  });
@@ -407,6 +440,15 @@ export class V1Collection {
407
440
  putSchema(schema, options = {}) { return putCollectionSchema(this.#transport, this.#params, schema, options); }
408
441
  /** @param {V1RequestOptions} [options] */
409
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); }
410
452
 
411
453
  /**
412
454
  * Iterates over every page, following `nextCursor`. `count`
@@ -814,6 +856,14 @@ export class V1Database {
814
856
  changes(request = {}, options = {}) {
815
857
  return streamChanges(this.#transport, this.name, request, options.signal);
816
858
  }
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
+ }
817
867
  }
818
868
 
819
869
  /**
@@ -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
@@ -12,6 +12,7 @@ export type V1ShapeDocument = import("./v1/shape-subscription.js").V1ShapeDocume
12
12
  export type V1SealedShapePosition = import("./v1/shape-reader.js").V1SealedShapePosition;
13
13
  export type EntityId = import("./ids.js").EntityId;
14
14
  export type TerraBaseV1ClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
15
+ export type V1TokenProvider = import("./v1/transport.js").V1TokenProvider;
15
16
  export type V1RequestOptions = import("./v1/terrabase-v1.js").V1RequestOptions;
16
17
  export type V1ReadOptions = import("./v1/terrabase-v1.js").V1ReadOptions;
17
18
  export type V1WriteOptions = import("./v1/terrabase-v1.js").V1WriteOptions;
@@ -20,6 +21,7 @@ export type V1ConditionalWriteOptions = import("./v1/terrabase-v1.js").V1Conditi
20
21
  export type V1PatchOp = import("./v1/terrabase-v1.js").V1PatchOp;
21
22
  export type V1Patch = import("./v1/terrabase-v1.js").V1Patch;
22
23
  export type V1SortKey = import("./v1/terrabase-v1.js").V1SortKey;
24
+ export type V1NearSortKey = import("./v1/terrabase-v1.js").V1NearSortKey;
23
25
  export type V1Query = import("./v1/terrabase-v1.js").V1Query;
24
26
  export type V1Count = import("./v1/terrabase-v1.js").V1Count;
25
27
  export type V1QueryPage = import("./v1/terrabase-v1.js").V1QueryPage;
@@ -57,6 +59,8 @@ export type V1SqlCheck = import("./v1/sql.js").V1SqlCheck;
57
59
  export type V1ChangeStreamRequest = import("./v1/changes.js").V1ChangeStreamRequest;
58
60
  export type V1DatabaseChange = import("./v1/changes.js").V1DatabaseChange;
59
61
  export type V1DatabaseChangeCursor = import("./v1/changes.js").V1DatabaseChangeCursor;
62
+ export type V1ExportOptions = import("./v1/export.js").V1ExportOptions;
63
+ export type V1ExportedDocument = import("./v1/export.js").V1ExportedDocument;
60
64
  export type V1ManagedDatabase = import("./v1/catalog.js").V1ManagedDatabase;
61
65
  export type V1DatabaseCreateRequest = import("./v1/catalog.js").V1DatabaseCreateRequest;
62
66
  export type V1DatabaseUpdateRequest = import("./v1/catalog.js").V1DatabaseUpdateRequest;
@@ -64,6 +68,7 @@ export type V1DatabaseBranchRequest = import("./v1/catalog.js").V1DatabaseBranch
64
68
  export type V1DatabaseListRequest = import("./v1/catalog.js").V1DatabaseListRequest;
65
69
  export type V1DatabasePage = import("./v1/catalog.js").V1DatabasePage;
66
70
  export type V1CollectionSchema = import("./v1/catalog.js").V1CollectionSchema;
71
+ export type V1CollectionTtl = import("./v1/catalog.js").V1CollectionTtl;
67
72
  export type V1InferredSchema = import("./v1/database-schema.js").V1InferredSchema;
68
73
  export type V1DatabaseSchema = import("./v1/database-schema.js").V1DatabaseSchema;
69
74
  export type V1UniqueSchema = import("./v1/database-schema.js").V1UniqueSchema;
@@ -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";
@@ -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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Streams every live document of a database as NDJSON, in collection then id order, from one read snapshot.
3
+ * A stream that ends inside a line is incomplete and throws; resume with `after` set to the last document received.
4
+ * @param {V1Transport} transport
5
+ * @param {string} database
6
+ * @param {V1ExportOptions} [options]
7
+ * @returns {AsyncGenerator<V1ExportedDocument, void, undefined>}
8
+ */
9
+ export function exportDocuments(transport: V1Transport, database: string, options?: V1ExportOptions): AsyncGenerator<V1ExportedDocument, void, undefined>;
10
+ export type V1ExportedDocument = {
11
+ readonly collection: string;
12
+ readonly id: string;
13
+ readonly document: JsonObject;
14
+ };
15
+ export type V1ExportOptions = {
16
+ /**
17
+ * Resume after this document; the resumed part reads a new snapshot.
18
+ */
19
+ after?: {
20
+ readonly collection: string;
21
+ readonly id: string;
22
+ } | undefined;
23
+ signal?: AbortSignal | undefined;
24
+ };
25
+ import type { V1Transport } from "./transport.js";
26
+ import type { JsonObject } from "./json.js";
@@ -1,3 +1,3 @@
1
1
  /** @type {readonly import("./routes.js").V1Route[]} */
2
2
  export const managementV1Routes: readonly import("./routes.js").V1Route[];
3
- export type ManagementOperation = "getShapeManifest" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery";
3
+ export type ManagementOperation = "getShapeManifest" | "exportDatabase" | "putCollectionTtl" | "getCollectionTtl" | "deleteCollectionTtl" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery";
@@ -9,7 +9,7 @@ export function decodeFreeShapePage(value: import("./json.js").JsonValue | undef
9
9
  */
10
10
  export function readShapePage(transport: V1Transport, params: Readonly<Record<string, string>>, request: V1ShapeRequest, signal: AbortSignal | undefined, ifNoneMatch?: string | undefined): Promise<V1ShapePage>;
11
11
  /**
12
- * Reuse process-memory keys for this handle/epoch/KID; fetch misses without HTTP cache reuse.
12
+ * Reuse keys only for static credentials; callback tokens authorize every decrypt with no-store.
13
13
  * @param {V1Transport} transport @param {Readonly<Record<string,string>>} params
14
14
  * @param {Uint8Array} bytes @param {V1SealedShapePosition} position @param {AbortSignal | undefined} signal
15
15
  * @returns {Promise<V1ShapePage>}
@@ -82,6 +82,15 @@ export class V1Collection {
82
82
  putSchema(schema: import("./json.js").JsonInputObject | boolean, options?: V1RequestOptions): Promise<import("./catalog.js").V1CollectionSchema>;
83
83
  /** @param {V1RequestOptions} [options] */
84
84
  deleteSchema(options?: V1RequestOptions): Promise<import("./catalog.js").V1CollectionSchema>;
85
+ /** @param {V1RequestOptions} [options] */
86
+ getTtl(options?: V1RequestOptions): Promise<import("./catalog.js").V1CollectionTtl>;
87
+ /**
88
+ * Documents whose member at `path` holds a past RFC 3339 UTC time are deleted by an hourly sweep (up to about an hour late).
89
+ * @param {string} path JSON Pointer such as `/expiresAt`. @param {V1RequestOptions} [options]
90
+ */
91
+ putTtl(path: string, options?: V1RequestOptions): Promise<import("./catalog.js").V1CollectionTtl>;
92
+ /** @param {V1RequestOptions} [options] */
93
+ deleteTtl(options?: V1RequestOptions): Promise<import("./catalog.js").V1CollectionTtl>;
85
94
  /**
86
95
  * Iterates over every page, following `nextCursor`. `count`
87
96
  * is sent with the first page only.
@@ -286,6 +295,11 @@ export class V1Database {
286
295
  explainSql(statement: string, options?: V1RequestOptions): Promise<import("./sql.js").V1SqlCheck>;
287
296
  /** @param {import("./changes.js").V1ChangeStreamRequest} [request] @param {V1RequestOptions} [options] */
288
297
  changes(request?: import("./changes.js").V1ChangeStreamRequest, options?: V1RequestOptions): AsyncGenerator<import("./changes.js").V1DatabaseChange | import("./changes.js").V1DatabaseChangeCursor, void, undefined>;
298
+ /**
299
+ * Streams every live document as `{ collection, id, document }`, in collection then id order, from one snapshot.
300
+ * @param {import("./export.js").V1ExportOptions} [options]
301
+ */
302
+ export(options?: import("./export.js").V1ExportOptions): AsyncGenerator<import("./export.js").V1ExportedDocument, void, undefined>;
289
303
  #private;
290
304
  }
291
305
  /**
@@ -385,17 +399,28 @@ export type V1Patch = {
385
399
  } | {
386
400
  readonly ops: readonly V1PatchOp[];
387
401
  };
388
- export type V1SortKey = {
402
+ export type V1FieldSortKey = {
389
403
  readonly path: V1Path;
390
404
  readonly order?: "asc" | "desc" | undefined;
391
405
  };
406
+ /**
407
+ * Vector similarity sort, nearest first. It must be the only sort key; limit is at most 100 and there is no cursor.
408
+ */
409
+ export type V1NearSortKey = {
410
+ readonly near: {
411
+ readonly path: V1Path;
412
+ readonly vector: readonly number[];
413
+ readonly metric?: "cosine" | "l2" | undefined;
414
+ };
415
+ };
416
+ export type V1SortKey = V1FieldSortKey | V1NearSortKey;
392
417
  /**
393
418
  * A query-v1 request. Every member is optional.
394
419
  */
395
420
  export type V1Query = {
396
421
  filter?: V1Filter | null | undefined;
397
422
  /**
398
- * At most 4 keys; results are finally ordered by `$id`.
423
+ * At most 4 keys, or one `near` key; results are finally ordered by `$id`.
399
424
  */
400
425
  sort?: readonly V1SortKey[] | undefined;
401
426
  /**
@@ -36,6 +36,8 @@ export class V1Transport {
36
36
  project?: undefined;
37
37
  endpointResolver?: undefined;
38
38
  }));
39
+ /** Internal credential lifetime metadata; never exposes the bearer value. */
40
+ get hasTokenProvider(): boolean;
39
41
  /**
40
42
  * @param {V1Operation} operation
41
43
  * @param {Readonly<Record<string, string>>} params
@@ -74,12 +76,12 @@ export class V1Transport {
74
76
  */
75
77
  call(operation: V1Operation, params: Readonly<Record<string, string>>, options?: V1CallOptions): Promise<V1CallResult>;
76
78
  /**
77
- * @param {"streamDatabaseChanges"} operation
79
+ * @param {"streamDatabaseChanges" | "exportDatabase"} operation
78
80
  * @param {Readonly<Record<string, string>>} params
79
81
  * @param {V1CallOptions} [options]
80
82
  * @returns {Promise<Response>}
81
83
  */
82
- openStream(operation: "streamDatabaseChanges", params: Readonly<Record<string, string>>, options?: V1CallOptions): Promise<Response>;
84
+ openStream(operation: "streamDatabaseChanges" | "exportDatabase", params: Readonly<Record<string, string>>, options?: V1CallOptions): Promise<Response>;
83
85
  #private;
84
86
  }
85
87
  /**
@@ -91,6 +93,10 @@ export type V1EndpointContext = {
91
93
  readonly signal?: AbortSignal | undefined;
92
94
  };
93
95
  export type V1EndpointResolver = (context: V1EndpointContext) => string | Promise<string>;
96
+ /**
97
+ * A fresh access token for each authenticated request, including stream reconnects.
98
+ */
99
+ export type V1TokenProvider = () => string | Promise<string>;
94
100
  /**
95
101
  * Project-scoped C# Documents API client. Supply exactly one of baseUrl or endpointResolver.
96
102
  * URLs must use HTTPS or loopback HTTP. Mutations are never automatically replayed.
@@ -98,9 +104,9 @@ export type V1EndpointResolver = (context: V1EndpointContext) => string | Promis
98
104
  export type TerraBaseV1ClientConfig = {
99
105
  project: string;
100
106
  /**
101
- * A ts-auth project access JWT (or a legacy C# tb_ key); held privately.
107
+ * 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.
102
108
  */
103
- token: string;
109
+ token: string | V1TokenProvider;
104
110
  baseUrl?: string | undefined;
105
111
  endpointResolver?: V1EndpointResolver | undefined;
106
112
  fetch?: FetchLike | undefined;