@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 +14 -0
- package/README.md +29 -0
- package/package.json +1 -1
- package/sdk-public-v1-route-manifest.json +4 -0
- package/src/index.js +5 -0
- package/src/v1/catalog.js +30 -0
- package/src/v1/decode.js +1 -0
- package/src/v1/export.js +68 -0
- package/src/v1/management-routes.js +8 -3
- package/src/v1/shape-reader.js +7 -5
- package/src/v1/terrabase-v1.js +54 -4
- package/src/v1/transport.js +48 -8
- package/types/index.d.ts +5 -0
- package/types/v1/catalog.d.ts +12 -0
- package/types/v1/decode.d.ts +5 -0
- package/types/v1/export.d.ts +26 -0
- package/types/v1/management-routes.d.ts +1 -1
- package/types/v1/shape-reader.d.ts +1 -1
- package/types/v1/terrabase-v1.d.ts +27 -2
- package/types/v1/transport.d.ts +10 -4
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
|
@@ -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
|
/**
|
package/src/v1/export.js
ADDED
|
@@ -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
|
}
|
package/src/v1/shape-reader.js
CHANGED
|
@@ -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>>}
|
|
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
|
|
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
|
|
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 });
|
package/src/v1/terrabase-v1.js
CHANGED
|
@@ -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 {
|
|
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 }}
|
|
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(
|
|
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
|
/**
|
package/src/v1/transport.js
CHANGED
|
@@ -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
|
|
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) &&
|
|
154
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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;
|
package/types/v1/catalog.d.ts
CHANGED
|
@@ -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";
|
package/types/v1/decode.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
/**
|
package/types/v1/transport.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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;
|