@terrascale/sdk 0.0.3 → 0.0.4

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +177 -67
  3. package/package.json +10 -4
  4. package/sdk-public-v1-route-manifest.json +81 -0
  5. package/sdk-v1-route-manifest.json +896 -15
  6. package/src/http.js +3 -48
  7. package/src/index.js +75 -1
  8. package/src/local/environment.js +0 -1
  9. package/src/ts-auth.js +1 -5
  10. package/src/v1/backups.js +111 -0
  11. package/src/v1/catalog.js +210 -0
  12. package/src/v1/changes.js +225 -0
  13. package/src/v1/database-schema.js +45 -0
  14. package/src/v1/decode.js +5 -5
  15. package/src/v1/errors.js +23 -3
  16. package/src/v1/ids.js +5 -2
  17. package/src/v1/imports.js +72 -0
  18. package/src/v1/indexes.js +76 -0
  19. package/src/v1/json.js +4 -3
  20. package/src/v1/management-decode.js +24 -0
  21. package/src/v1/management-routes.js +67 -0
  22. package/src/v1/management-validation.js +25 -0
  23. package/src/v1/routes.js +32 -7
  24. package/src/v1/saved-queries.js +58 -0
  25. package/src/v1/shape-crypto.js +125 -0
  26. package/src/v1/shape-manifest.js +31 -0
  27. package/src/v1/shape-reader.js +212 -0
  28. package/src/v1/shape-subscription.js +208 -0
  29. package/src/v1/shapes.js +11 -5
  30. package/src/v1/sql.js +98 -0
  31. package/src/v1/system.js +80 -0
  32. package/src/v1/terrabase-v1.js +178 -27
  33. package/src/v1/transport.js +75 -31
  34. package/src/v1/usage.js +101 -0
  35. package/types/http.d.ts +2 -8
  36. package/types/index.d.ts +68 -0
  37. package/types/local/environment.d.ts +1 -1
  38. package/types/ts-auth.d.ts +0 -2
  39. package/types/v1/backups.d.ts +128 -0
  40. package/types/v1/catalog.d.ts +92 -0
  41. package/types/v1/changes.d.ts +53 -0
  42. package/types/v1/database-schema.d.ts +82 -0
  43. package/types/v1/decode.d.ts +15 -4
  44. package/types/v1/errors.d.ts +16 -1
  45. package/types/v1/ids.d.ts +2 -2
  46. package/types/v1/imports.d.ts +132 -0
  47. package/types/v1/indexes.d.ts +75 -0
  48. package/types/v1/management-decode.d.ts +15 -0
  49. package/types/v1/management-routes.d.ts +3 -0
  50. package/types/v1/management-validation.d.ts +8 -0
  51. package/types/v1/routes.d.ts +5 -2
  52. package/types/v1/saved-queries.d.ts +33 -0
  53. package/types/v1/shape-crypto.d.ts +39 -0
  54. package/types/v1/shape-manifest.d.ts +28 -0
  55. package/types/v1/shape-reader.d.ts +45 -0
  56. package/types/v1/shape-subscription.d.ts +35 -0
  57. package/types/v1/shapes.d.ts +16 -2
  58. package/types/v1/sql.d.ts +47 -0
  59. package/types/v1/system.d.ts +85 -0
  60. package/types/v1/terrabase-v1.d.ts +113 -4
  61. package/types/v1/transport.d.ts +25 -8
  62. package/types/v1/usage.d.ts +171 -0
  63. package/src/ts-auth-discovery.js +0 -207
  64. package/types/ts-auth-discovery.d.ts +0 -19
@@ -0,0 +1,208 @@
1
+ // Client state for one collection shape: bootstrap, snapshot pages, log chunks, then the live tail.
2
+ import { TerraBaseV1Error } from "./errors.js";
3
+ import { requireFilter } from "./filter.js";
4
+ import { parseExactJson, stringifyExactJson } from "./json.js";
5
+ import { readShapePage } from "./shape-reader.js";
6
+ /**
7
+ * @import { JsonObject } from "./json.js"
8
+ * @import { V1Filter } from "./filter.js"
9
+ * @import { V1Transport } from "./transport.js"
10
+ */
11
+ /**
12
+ * @typedef {object} V1ShapeSubscribeOptions
13
+ * @property {AbortSignal | undefined} [signal] Closes the subscription when aborted; cancellation settles `done`.
14
+ * @property {((subscription: V1ShapeSubscription) => void) | undefined} [onUpdate] Runs after validated responses and freshness changes; throwing rejects `done` unless already closed or aborted.
15
+ */
16
+ /** @typedef {{ readonly version: string; readonly document: JsonObject }} V1ShapeDocument */
17
+ const retryBaseMilliseconds = 250;
18
+ const retryCapMilliseconds = 30_000;
19
+ // The eighth attempt reaches the delay ceiling; retries continue until success or cancellation.
20
+ const maximumBackoffAttempt = 8;
21
+ /** @param {import("./errors.js").TerraBaseV1Error} error @returns {boolean} */
22
+ function isRetryable(error) {
23
+ return (error.status === 429 && error.code === "shape_busy") ||
24
+ (error.status === 503 && (error.code === "unavailable" || error.code === "shape_too_large"));
25
+ }
26
+ /** @param {number} attempt @param {number | undefined} retryAfter @returns {number} */
27
+ function retryDelay(attempt, retryAfter) {
28
+ return Math.max(Math.min(retryCapMilliseconds, retryBaseMilliseconds * 2 ** (attempt - 1)), (retryAfter ?? 0) * 1000);
29
+ }
30
+ /** @param {unknown} value @param {readonly string[]} allowed @param {string} what */
31
+ function requireMembers(value, allowed, what) {
32
+ if (typeof value !== "object" || value === null) throw new TypeError(`${what} is an object.`);
33
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new TypeError(`Unknown ${what} member ${key}.`);
34
+ }
35
+ /** @template T @param {T} value @returns {T} */
36
+ function deepFreeze(value) {
37
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
38
+ Object.freeze(value);
39
+ for (const item of Object.values(value)) deepFreeze(item);
40
+ }
41
+ return value;
42
+ }
43
+ // setTimeout overflows above 2^31-1 ms and fires at once, so a long Retry-After is waited in chunks.
44
+ const maximumTimerMilliseconds = 2 ** 31 - 1;
45
+ /** @param {number} milliseconds @param {AbortSignal} signal @returns {Promise<void>} */
46
+ function sleep(milliseconds, signal) {
47
+ return new Promise((resolve, reject) => {
48
+ signal.throwIfAborted();
49
+ let remaining = milliseconds;
50
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
51
+ let timer;
52
+ const onAbort = () => { clearTimeout(timer); reject(signal.reason); };
53
+ const wait = () => {
54
+ const chunk = Math.min(remaining, maximumTimerMilliseconds);
55
+ timer = setTimeout(() => {
56
+ remaining -= chunk;
57
+ if (remaining > 0) wait();
58
+ else { signal.removeEventListener("abort", onAbort); resolve(); }
59
+ }, chunk);
60
+ };
61
+ signal.addEventListener("abort", onAbort, { once: true });
62
+ wait();
63
+ });
64
+ }
65
+ export class V1ShapeSubscription {
66
+ /** @type {Map<string, V1ShapeDocument>} */
67
+ #documents = new Map();
68
+ /** @type {string | undefined} */
69
+ #handle;
70
+ #offset = "-1";
71
+ /** @type {string | undefined} */
72
+ #cursor;
73
+ /** @type {string | undefined} Usable only from a snapshot-complete or log position. */
74
+ #resume;
75
+ #upToDate = false;
76
+ // Set after a changed-handle 409 while the saved resume is still usable; cleared by any commit or reset.
77
+ #resumeAttempt = false;
78
+ #controller = new AbortController();
79
+ #transport;
80
+ #params;
81
+ /** @type {V1Filter | undefined} */
82
+ #where;
83
+ #onUpdate;
84
+ #external;
85
+ #onExternalAbort = () => this.close();
86
+ #done;
87
+ /**
88
+ * @param {V1Transport} transport
89
+ * @param {Readonly<Record<string, string>>} params
90
+ * @param {{ where?: V1Filter | undefined }} [shape]
91
+ * @param {V1ShapeSubscribeOptions} [options]
92
+ */
93
+ constructor(transport, params, shape = {}, options = {}) {
94
+ requireMembers(shape, ["where"], "shape");
95
+ requireMembers(options, ["signal", "onUpdate"], "shape subscribe option");
96
+ const { where } = shape;
97
+ const { signal, onUpdate } = options;
98
+ if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError("signal is an AbortSignal.");
99
+ if (onUpdate !== undefined && typeof onUpdate !== "function") throw new TypeError("onUpdate is a function.");
100
+ this.#transport = transport;
101
+ this.#params = Object.freeze({ ...params });
102
+ // Exact copy: a structuredClone would lose the source text of large or fractional JSON numbers.
103
+ this.#where = where === undefined ? undefined : requireFilter(parseExactJson(stringifyExactJson(requireFilter(where), "shape filter")));
104
+ this.#onUpdate = onUpdate;
105
+ this.#external = signal;
106
+ if (signal?.aborted === true) this.close();
107
+ else signal?.addEventListener("abort", this.#onExternalAbort);
108
+ this.#done = this.#run();
109
+ // Callback-only consumers must not trigger a process-level unhandled rejection.
110
+ void this.#done.catch(() => {});
111
+ }
112
+ /** A copy per read: callers cannot change the documents this subscription holds. */
113
+ get state() { return new Map(this.#documents); }
114
+ get upToDate() { return this.#upToDate; }
115
+ /** Settles when the run ends: resolves on close, rejects on a terminal failure. */
116
+ get done() { return this.#done; }
117
+ close() { this.#controller.abort(); }
118
+ /** @returns {Promise<void>} */
119
+ async #run() {
120
+ const signal = this.#controller.signal;
121
+ let retries = 0;
122
+ try {
123
+ for (;;) {
124
+ signal.throwIfAborted();
125
+ const request = this.#request();
126
+ let page;
127
+ try {
128
+ page = await readShapePage(this.#transport, this.#params, request, signal);
129
+ } catch (error) {
130
+ if (!(error instanceof TerraBaseV1Error)) throw error;
131
+ if (isRetryable(error)) {
132
+ retries = Math.min(retries + 1, maximumBackoffAttempt);
133
+ await sleep(retryDelay(retries, error.retryAfter), signal);
134
+ continue;
135
+ }
136
+ if (error.status !== 409 || error.code !== "must_refetch") throw error;
137
+ this.#recover(request, error);
138
+ continue;
139
+ }
140
+ retries = 0;
141
+ this.#commit(page);
142
+ this.#onUpdate?.(this);
143
+ }
144
+ } catch (error) {
145
+ if (signal.aborted) return;
146
+ throw error;
147
+ } finally {
148
+ // A finished run must not keep the caller's signal (and this subscription) alive.
149
+ this.#external?.removeEventListener("abort", this.#onExternalAbort);
150
+ }
151
+ }
152
+ /** @returns {import("./shapes.js").V1ShapeRequest} */
153
+ #request() {
154
+ if (this.#resumeAttempt) return { where: this.#where, offset: "-1", cursor: this.#resume };
155
+ if (this.#handle === undefined) return { where: this.#where, offset: "-1" };
156
+ // Only a fresh head is a live tail; snapshot positions are never live.
157
+ if (this.#upToDate && !this.#offset.startsWith("snap.")) return { where: this.#where, offset: this.#offset, handle: this.#handle, live: true, cursor: this.#cursor };
158
+ return { where: this.#where, offset: this.#offset, handle: this.#handle };
159
+ }
160
+ /**
161
+ * @param {import("./shapes.js").V1ShapeRequest} request
162
+ * @param {TerraBaseV1Error} error
163
+ */
164
+ #recover(request, error) {
165
+ if (this.#resumeAttempt) return this.#reset();
166
+ if (request.handle === undefined) throw error;
167
+ // §7.6 reads the lineage from the response header. A body that names another handle is ambiguous: fail closed.
168
+ const handle = error.headers.get("tb-shape-handle");
169
+ const changed = handle !== null && handle !== this.#handle && (error.handle === undefined || error.handle === handle);
170
+ if (changed && this.#resume !== undefined) {
171
+ // One attempt on the new lineage; its own 409 falls through to #reset above.
172
+ this.#resumeAttempt = true;
173
+ if (this.#upToDate) {
174
+ this.#upToDate = false;
175
+ this.#onUpdate?.(this);
176
+ }
177
+ return;
178
+ }
179
+ this.#reset();
180
+ }
181
+ #reset() {
182
+ this.#documents = new Map();
183
+ this.#handle = undefined;
184
+ this.#offset = "-1";
185
+ this.#cursor = undefined;
186
+ this.#resume = undefined;
187
+ this.#upToDate = false;
188
+ this.#resumeAttempt = false;
189
+ this.#onUpdate?.(this);
190
+ }
191
+ /** @param {import("./shapes.js").V1ShapePage} page */
192
+ #commit(page) {
193
+ // Repeats are applied, never skipped: a repeated event can carry a newer materialized value.
194
+ const documents = new Map(this.#documents);
195
+ for (const change of page.changes)
196
+ for (const op of change.ops) {
197
+ if (op.op === "delete") documents.delete(op.id);
198
+ else documents.set(op.id, deepFreeze({ version: op.version, document: op.document }));
199
+ }
200
+ this.#documents = documents;
201
+ this.#handle = page.handle;
202
+ this.#offset = page.nextOffset;
203
+ this.#cursor = page.cursor;
204
+ this.#resume = page.nextOffset.startsWith("snap.") ? undefined : page.cursorResume;
205
+ this.#upToDate = page.upToDate;
206
+ this.#resumeAttempt = false;
207
+ }
208
+ }
package/src/v1/shapes.js CHANGED
@@ -16,7 +16,7 @@ import { stringifyExactJson } from "./json.js";
16
16
  * @property {boolean | undefined} [live] True only for log positions; waits up to 20 seconds.
17
17
  * @property {string | undefined} [cursor] Previous live response's TB-Shape-Cursor.
18
18
  */
19
- /** @typedef {{ readonly op: "upsert"; readonly id: import("../ids.js").EntityId; readonly version: string; readonly document: JsonObject } | { readonly op: "delete"; readonly id: import("../ids.js").EntityId }} V1ShapeOp */
19
+ /** @typedef {{ readonly op: "upsert"; readonly id: string; readonly version: string; readonly document: JsonObject } | { readonly op: "delete"; readonly id: string }} V1ShapeOp */
20
20
  /** @typedef {{ readonly offset: string; readonly ops: readonly V1ShapeOp[] }} V1ShapeChange */
21
21
  /**
22
22
  * One snapshot page or log chunk. Only a fresh open tail/live response can report upToDate.
@@ -29,6 +29,9 @@ import { stringifyExactJson } from "./json.js";
29
29
  * @property {readonly V1ShapeChange[]} changes Apply commits and operations in their received order.
30
30
  * @property {string | undefined} cursor Echo this as cursor on the next live request.
31
31
  * @property {string | undefined} etag
32
+ * @property {string | undefined} [coveredOffset] Free-read protocol coverage (feed sequence, not write offset).
33
+ * @property {string | undefined} [valuesAt] Free-read materialization position.
34
+ * @property {string | undefined} [cursorResume] Opaque free-read resume token.
32
35
  */
33
36
  const maximumOffset = (1n << 63n) - 1n;
34
37
  const handlePattern = /^h1-[0-9a-f]{16}$/u;
@@ -59,7 +62,7 @@ export function requireShapeOffset(value) {
59
62
  throw new TypeError("A shape offset is -1, a signed-64-bit decimal log position, or snap.<head>.<canonical-base64url-id>.");
60
63
  }
61
64
  /** @param {unknown} value @returns {string} */
62
- function requireHandle(value) {
65
+ export function requireShapeHandle(value) {
63
66
  if (typeof value !== "string" || !handlePattern.test(value)) throw new TypeError("A shape handle is h1- followed by 16 lowercase hex characters.");
64
67
  return value;
65
68
  }
@@ -72,7 +75,7 @@ export function encodeShapeRequest(request) {
72
75
  if (request.where !== undefined) query.set("where", stringifyExactJson(requireFilter(request.where), "shape filter"));
73
76
  const offset = requireShapeOffset(request.offset ?? "-1");
74
77
  query.set("offset", offset);
75
- if (request.handle !== undefined) query.set("handle", requireHandle(request.handle));
78
+ if (request.handle !== undefined) query.set("handle", requireShapeHandle(request.handle));
76
79
  else if (offset !== "-1") throw new TypeError("A shape handle is required after bootstrap.");
77
80
  if (request.live !== undefined && typeof request.live !== "boolean") throw new TypeError("live is a boolean.");
78
81
  if (request.live === true) {
@@ -94,10 +97,13 @@ export function decodeShapePage(value, headers) {
94
97
  const cursor = headers.get("tb-shape-cursor") ?? undefined;
95
98
  if (cursor !== undefined && !isLogOffset(cursor)) malformed("shape cursor");
96
99
  return Object.freeze({
97
- shapeId, handle: requireHandle(raw.handle),
100
+ shapeId, handle: requireShapeHandle(raw.handle),
98
101
  offset: requireShapeOffset(raw.offset), nextOffset: requireShapeOffset(raw.next_offset),
99
102
  upToDate: boolean(raw.up_to_date, "shape up_to_date"), cursor,
100
103
  etag: headers.get("etag") ?? undefined,
104
+ ...(raw.covered_offset === undefined ? {} : { coveredOffset: requireShapeOffset(raw.covered_offset) }),
105
+ ...(raw.values_at === undefined ? {} : { valuesAt: requireShapeOffset(raw.values_at) }),
106
+ ...(raw.cursor_resume === undefined ? {} : { cursorResume: string(raw.cursor_resume, "shape resume cursor") }),
101
107
  changes: Object.freeze(array(raw.changes, "shape changes").map(value => {
102
108
  const change = object(value, "shape change");
103
109
  return Object.freeze({ offset: u64(change.offset, "shape change offset"), ops: Object.freeze(array(change.ops, "shape operations").map(value => {
@@ -130,7 +136,7 @@ export async function readShape(transport, params, request, signal) {
130
136
  const target = new URL(location, source);
131
137
  if (target.origin !== source.origin || target.pathname !== source.pathname || target.username !== "" || target.password !== "" || target.hash !== "")
132
138
  malformed("shape redirect target");
133
- const handle = requireHandle(target.searchParams.get("handle"));
139
+ const handle = requireShapeHandle(target.searchParams.get("handle"));
134
140
  const offset = requireShapeOffset(target.searchParams.get("offset"));
135
141
  if (!offset.startsWith("snap.")) malformed("shape bootstrap offset");
136
142
  query.set("handle", handle);
package/src/v1/sql.js ADDED
@@ -0,0 +1,98 @@
1
+ import { array, boolean, malformed, object, string } from "./decode.js";
2
+ import { requireDocumentId, requireName } from "./ids.js";
3
+ /**
4
+ * @import { JsonObject, JsonValue } from "./json.js"
5
+ * @import { V1RequestOptions } from "./terrabase-v1.js"
6
+ * @import { V1Transport } from "./transport.js"
7
+ */
8
+
9
+ /** @typedef {{ readonly id: string; readonly value: JsonObject }} V1SqlRow */
10
+ /**
11
+ * SQL returns one bounded result, without a pagination cursor.
12
+ * @typedef {{ readonly matched: number; readonly truncated: boolean; readonly rows: readonly V1SqlRow[] }} V1SqlResult
13
+ */
14
+ /** @typedef {{ readonly field: string; readonly reason: string }} V1SqlIndexSuggestion */
15
+ /**
16
+ * @typedef {{
17
+ * readonly ok: boolean;
18
+ * readonly error: string | null;
19
+ * readonly collection: string | null;
20
+ * readonly hasLimit: boolean;
21
+ * readonly plan: readonly string[];
22
+ * readonly indexes: readonly string[];
23
+ * readonly suggestions: readonly V1SqlIndexSuggestion[];
24
+ * }} V1SqlCheck
25
+ */
26
+
27
+ /** @param {string} sql @param {V1RequestOptions} options @returns {string} */
28
+ function requestBody(sql, options) {
29
+ if (typeof sql !== "string" || sql.length > 16_384)
30
+ throw new TypeError("SQL must be a string of at most 16384 UTF-16 code units.");
31
+ if (options === null || typeof options !== "object" || Array.isArray(options))
32
+ throw new TypeError("SQL request options must be an object.");
33
+ for (const member of Object.keys(options))
34
+ if (member !== "signal") throw new TypeError(`Unknown SQL request option ${member}.`);
35
+ // SQL syntax, including empty statements, stays producer-owned: check returns ok:false.
36
+ return JSON.stringify({ sql });
37
+ }
38
+
39
+ /** @param {JsonValue | undefined} value @returns {V1SqlResult} */
40
+ function decodeResult(value) {
41
+ const raw = object(value, "SQL result");
42
+ const rows = array(raw.rows, "SQL rows").map(entry => {
43
+ const row = object(entry, "SQL row");
44
+ return Object.freeze({ id: requireDocumentId(row.id), value: object(row.value, "SQL row value") });
45
+ });
46
+ const matched = raw.matched;
47
+ if (typeof matched !== "number" || !Number.isInteger(matched) || matched < 0 || matched > 1000 || matched !== rows.length)
48
+ malformed("SQL matched count");
49
+ if (new Set(rows.map(row => row.id)).size !== rows.length) malformed("SQL row identities");
50
+ return Object.freeze({ matched, truncated: boolean(raw.truncated, "SQL truncation flag"), rows: Object.freeze(rows) });
51
+ }
52
+
53
+ /** @param {JsonValue | undefined} value @param {"checkSql" | "explainSql"} operation @returns {V1SqlCheck} */
54
+ function decodeCheck(value, operation) {
55
+ const raw = object(value, "SQL check");
56
+ const ok = boolean(raw.ok, "SQL check flag");
57
+ const error = raw.error === null ? null : string(raw.error, "SQL check error");
58
+ const collection = raw.collection === null ? null : requireName("collection", raw.collection);
59
+ const hasLimit = boolean(raw.hasLimit, "SQL limit flag");
60
+ const plan = array(raw.plan, "SQL plan").map(step => string(step, "SQL plan step"));
61
+ const indexes = array(raw.indexes, "SQL indexes").map(index => string(index, "SQL index"));
62
+ const suggestions = array(raw.suggestions, "SQL index suggestions").map(entry => {
63
+ const suggestion = object(entry, "SQL index suggestion");
64
+ return Object.freeze({ field: string(suggestion.field, "SQL index field"), reason: string(suggestion.reason, "SQL index reason") });
65
+ });
66
+ if (ok ? error !== null || collection === null : error === null || error === "" || collection !== null || hasLimit || plan.length > 0 || indexes.length > 0 || suggestions.length > 0)
67
+ malformed("SQL check outcome");
68
+ if (operation === "explainSql" && !ok) malformed("SQL explain outcome");
69
+ return Object.freeze({ ok, error, collection, hasLimit, plan: Object.freeze(plan), indexes: Object.freeze(indexes), suggestions: Object.freeze(suggestions) });
70
+ }
71
+
72
+ /**
73
+ * Runs the producer's read-only SQL subset; it never converts a legacy SQL builder.
74
+ * @param {V1Transport} transport
75
+ * @param {string} database
76
+ * @param {string} sql
77
+ * @param {V1RequestOptions} [options]
78
+ * @returns {Promise<V1SqlResult>}
79
+ */
80
+ export async function runSql(transport, database, sql, options = {}) {
81
+ const body = requestBody(sql, options);
82
+ return decodeResult((await transport.call("runSql", { database: requireName("database", database) }, { body, signal: options.signal })).value);
83
+ }
84
+
85
+ /**
86
+ * Check reports invalid SQL in a successful body; explain reports a producer HTTP error.
87
+ * @param {V1Transport} transport
88
+ * @param {string} database
89
+ * @param {string} sql
90
+ * @param {V1RequestOptions} [options]
91
+ * @param {"checkSql" | "explainSql"} [operation]
92
+ * @returns {Promise<V1SqlCheck>}
93
+ */
94
+ export async function checkSql(transport, database, sql, options = {}, operation = "checkSql") {
95
+ if (operation !== "checkSql" && operation !== "explainSql") throw new TypeError("SQL checks use checkSql or explainSql.");
96
+ const body = requestBody(sql, options);
97
+ return decodeCheck((await transport.call(operation, { database: requireName("database", database) }, { body, signal: options.signal })).value, operation);
98
+ }
@@ -0,0 +1,80 @@
1
+ import { decodeDocument } from "./decode.js";
2
+ import { requireId } from "../ids.js";
3
+ import { requireDocumentId, requireName } from "./ids.js";
4
+ import { flag, list, nullable, numeric, record, text } from "./management-decode.js";
5
+ import { members } from "./management-validation.js";
6
+ import { decodeCollection, decodeQueryPage } from "./terrabase-v1.js";
7
+ import { V1Transport } from "./transport.js";
8
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
9
+ const status = record({ status: text, version: text, region: text, storageFormat: text, revision: nullable(text), machine: text });
10
+ const replication = record({ region: text, status: text, maxLagSeconds: nullable(numeric),
11
+ peers: list(record({ region: text, status: text, lagSeconds: nullable(numeric) })) });
12
+ const databases = record({ items: list(record({ name: text, id: requireId, homeRegion: text, replicaRegions: list(text), allRegions: flag })) });
13
+ /** @typedef {ReturnType<typeof status> & {readonly httpStatus: number}} V1ProbeStatus */
14
+ /** @typedef {ReturnType<typeof replication>} V1ReplicationHealth */
15
+ /** @typedef {ReturnType<typeof databases>} V1PlatformDatabases */
16
+ /** @typedef {{ readonly baseUrl: string; readonly token?: string; readonly fetch?: import("../config.js").FetchLike; readonly maxResponseBytes?: number }} TerraBaseSystemClientConfig */
17
+
18
+ /** Public anonymous probes; 503 readiness/backup bodies remain typed probe outcomes. */
19
+ export class V1Health {
20
+ /** @type {V1Transport} */
21
+ #transport;
22
+ /** @param {V1Transport} transport */
23
+ constructor(transport) { this.#transport = transport; Object.freeze(this); }
24
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1ProbeStatus>} */
25
+ async liveness(options = {}) {
26
+ const response = await this.#transport.call("healthz", {}, { signal: options.signal });
27
+ return Object.freeze({ ...status(response.value), httpStatus: response.status });
28
+ }
29
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1ProbeStatus>} */
30
+ async readiness(options = {}) {
31
+ const response = await this.#transport.call("readyz", {}, { signal: options.signal });
32
+ return Object.freeze({ ...status(response.value), httpStatus: response.status });
33
+ }
34
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1ProbeStatus>} */
35
+ async backup(options = {}) {
36
+ const response = await this.#transport.call("backupz", {}, { signal: options.signal });
37
+ return Object.freeze({ ...status(response.value), httpStatus: response.status });
38
+ }
39
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1ReplicationHealth>} */
40
+ async replication(options = {}) { return replication((await this.#transport.call("replicationHealth", {}, { signal: options.signal })).value); }
41
+ }
42
+ /** The public read-only explore API requires a user JWT carrying platform_role=admin. */
43
+ export class V1Platform {
44
+ /** @type {V1Transport} */
45
+ #transport;
46
+ /** @param {V1Transport} transport */
47
+ constructor(transport) { this.#transport = transport; Object.freeze(this); }
48
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1PlatformDatabases>} */
49
+ async databases(options = {}) { return databases((await this.#transport.call("listPlatformDatabases", {}, { signal: options.signal })).value); }
50
+ /** @param {"ts-auth" | "ts-dashboard"} database @param {V1RequestOptions} [options] */
51
+ async collections(database, options = {}) {
52
+ const response = await this.#transport.call("listPlatformCollections", { database: requireName("database", database) }, { signal: options.signal });
53
+ return record({ items: list(decodeCollection), next_cursor: nullable(text) })(response.value);
54
+ }
55
+ /** @param {"ts-auth" | "ts-dashboard"} database @param {string} collection @param {{ readonly limit?: number; readonly cursor?: string }} [request] @param {V1RequestOptions} [options] */
56
+ async documents(database, collection, request = {}, options = {}) {
57
+ members(request, ["limit", "cursor"]);
58
+ const query = new URLSearchParams();
59
+ if (request.limit !== undefined) {
60
+ if (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 1000) throw new TypeError("limit must be 1-1000.");
61
+ query.set("limit", String(request.limit));
62
+ }
63
+ if (request.cursor !== undefined) {
64
+ if (typeof request.cursor !== "string" || request.cursor === "") throw new TypeError("cursor must be non-empty.");
65
+ query.set("cursor", request.cursor);
66
+ }
67
+ return decodeQueryPage((await this.#transport.call("listPlatformDocuments", { database: requireName("database", database), collection: requireName("collection", collection) }, { query, signal: options.signal })).value);
68
+ }
69
+ /** @param {"ts-auth" | "ts-dashboard"} database @param {string} collection @param {string} id @param {V1RequestOptions} [options] */
70
+ async document(database, collection, id, options = {}) {
71
+ return decodeDocument((await this.#transport.call("getPlatformDocument", { database: requireName("database", database), collection: requireName("collection", collection) }, {
72
+ query: new URLSearchParams({ id: requireDocumentId(id) }), signal: options.signal,
73
+ })).value);
74
+ }
75
+ }
76
+ /** @param {TerraBaseSystemClientConfig} config */
77
+ export function createTerraBaseSystemClient(config) {
78
+ const transport = new V1Transport(config);
79
+ return Object.freeze({ health: new V1Health(transport), platform: new V1Platform(transport) });
80
+ }