@terrascale/sdk 0.0.2 → 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 (84) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +193 -56
  3. package/package.json +14 -5
  4. package/sdk-public-v1-route-manifest.json +81 -0
  5. package/sdk-v1-route-manifest.json +8996 -1697
  6. package/src/database-codec.js +21 -26
  7. package/src/database-types.js +11 -11
  8. package/src/database-view.js +6 -6
  9. package/src/http.js +13 -48
  10. package/src/ids.js +22 -0
  11. package/src/index.js +77 -2
  12. package/src/local/better-auth.js +17 -6
  13. package/src/local/environment.js +0 -1
  14. package/src/local/index.js +2 -2
  15. package/src/local/legacy-client.js +5 -5
  16. package/src/local/router.js +12 -12
  17. package/src/local/store.js +29 -28
  18. package/src/local/test-environment.js +3 -2
  19. package/src/postgres.js +5 -4
  20. package/src/react/core.js +2 -2
  21. package/src/ts-auth-ids.js +122 -0
  22. package/src/ts-auth.js +75 -77
  23. package/src/v1/backups.js +111 -0
  24. package/src/v1/catalog.js +210 -0
  25. package/src/v1/changes.js +225 -0
  26. package/src/v1/database-schema.js +45 -0
  27. package/src/v1/decode.js +20 -8
  28. package/src/v1/errors.js +23 -3
  29. package/src/v1/ids.js +9 -59
  30. package/src/v1/imports.js +72 -0
  31. package/src/v1/indexes.js +76 -0
  32. package/src/v1/json.js +4 -3
  33. package/src/v1/management-decode.js +24 -0
  34. package/src/v1/management-routes.js +67 -0
  35. package/src/v1/management-validation.js +25 -0
  36. package/src/v1/routes.js +36 -11
  37. package/src/v1/saved-queries.js +58 -0
  38. package/src/v1/shape-crypto.js +125 -0
  39. package/src/v1/shape-manifest.js +31 -0
  40. package/src/v1/shape-reader.js +212 -0
  41. package/src/v1/shape-subscription.js +208 -0
  42. package/src/v1/shapes.js +15 -10
  43. package/src/v1/sql.js +98 -0
  44. package/src/v1/system.js +80 -0
  45. package/src/v1/terrabase-v1.js +192 -33
  46. package/src/v1/transport.js +80 -34
  47. package/src/v1/usage.js +101 -0
  48. package/types/database-codec.d.ts +1 -6
  49. package/types/database-types.d.ts +11 -11
  50. package/types/http.d.ts +2 -8
  51. package/types/ids.d.ts +10 -0
  52. package/types/index.d.ts +70 -1
  53. package/types/local/environment.d.ts +1 -1
  54. package/types/local/store.d.ts +10 -0
  55. package/types/local/test-environment.d.ts +2 -2
  56. package/types/postgres.d.ts +4 -4
  57. package/types/ts-auth-ids.d.ts +14 -0
  58. package/types/ts-auth.d.ts +99 -101
  59. package/types/v1/backups.d.ts +128 -0
  60. package/types/v1/catalog.d.ts +92 -0
  61. package/types/v1/changes.d.ts +53 -0
  62. package/types/v1/database-schema.d.ts +82 -0
  63. package/types/v1/decode.d.ts +17 -4
  64. package/types/v1/errors.d.ts +16 -1
  65. package/types/v1/ids.d.ts +4 -16
  66. package/types/v1/imports.d.ts +132 -0
  67. package/types/v1/indexes.d.ts +75 -0
  68. package/types/v1/management-decode.d.ts +15 -0
  69. package/types/v1/management-routes.d.ts +3 -0
  70. package/types/v1/management-validation.d.ts +8 -0
  71. package/types/v1/routes.d.ts +5 -2
  72. package/types/v1/saved-queries.d.ts +33 -0
  73. package/types/v1/shape-crypto.d.ts +39 -0
  74. package/types/v1/shape-manifest.d.ts +28 -0
  75. package/types/v1/shape-reader.d.ts +45 -0
  76. package/types/v1/shape-subscription.d.ts +35 -0
  77. package/types/v1/shapes.d.ts +14 -0
  78. package/types/v1/sql.d.ts +47 -0
  79. package/types/v1/system.d.ts +85 -0
  80. package/types/v1/terrabase-v1.d.ts +116 -7
  81. package/types/v1/transport.d.ts +27 -10
  82. package/types/v1/usage.d.ts +171 -0
  83. package/src/ts-auth-discovery.js +0 -207
  84. package/types/ts-auth-discovery.d.ts +0 -19
@@ -15,10 +15,12 @@ import { v1Route, v1RoutePath } from "./routes.js";
15
15
  export const maximumRequestBytes = 4 * 1024 * 1024;
16
16
  // Query budgets count stored JSON before wrappers and serializer escaping.
17
17
  const defaultMaximumResponseBytes = 64 * 1024 * 1024;
18
- const tokenPattern = /^tb_[A-Za-z0-9_-]{43}$/u;
18
+ // Validate header-safe credential syntax only; the producer validates JWT claims/signatures.
19
+ // Retain C# keys; current project routes also authenticate project access JWTs.
20
+ const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|tb_[A-Za-z0-9_-]{43})$/u;
19
21
  /**
20
22
  * Resolver input contains no credential. A successful URL is cached for this client/database.
21
- * @typedef {{ readonly project: string; readonly database: string; readonly signal?: AbortSignal | undefined }} V1EndpointContext
23
+ * @typedef {{ readonly project: string | undefined; readonly database?: string | undefined; readonly signal?: AbortSignal | undefined }} V1EndpointContext
22
24
  */
23
25
  /** @typedef {(context: V1EndpointContext) => string | Promise<string>} V1EndpointResolver */
24
26
  /**
@@ -26,7 +28,7 @@ const tokenPattern = /^tb_[A-Za-z0-9_-]{43}$/u;
26
28
  * URLs must use HTTPS or loopback HTTP. Mutations are never automatically replayed.
27
29
  * @typedef {object} TerraBaseV1ClientConfig
28
30
  * @property {string} project
29
- * @property {string} token A tb_ API key; held privately.
31
+ * @property {string} token A ts-auth project access JWT (or a legacy C# tb_ key); held privately.
30
32
  * @property {string | undefined} [baseUrl]
31
33
  * @property {V1EndpointResolver | undefined} [endpointResolver]
32
34
  * @property {FetchLike | undefined} [fetch]
@@ -35,11 +37,12 @@ const tokenPattern = /^tb_[A-Za-z0-9_-]{43}$/u;
35
37
 
36
38
  /**
37
39
  * @typedef {object} V1CallOptions
38
- * @property {string} [body]
40
+ * @property {string | Uint8Array} [body]
39
41
  * @property {string} [contentType]
40
42
  * @property {Readonly<Record<string, string>>} [headers] Contract headers only (If-Match, If-None-Match).
41
43
  * @property {URLSearchParams | undefined} [query]
42
44
  * @property {AbortSignal | undefined} [signal]
45
+ * @property {"no-store" | undefined} [cache] Authenticated shape epoch keys must bypass caches.
43
46
  */
44
47
 
45
48
  /**
@@ -123,32 +126,32 @@ export class BodyTransportError extends Error {
123
126
  }
124
127
  }
125
128
 
126
- /** Internal transport bound to one data key. */
129
+ /** Internal transport bound to one project credential. */
127
130
  export class V1Transport {
128
- /** @type {string} */
131
+ /** @type {string | undefined} */
129
132
  #project;
130
133
  /** @type {V1EndpointResolver} */
131
134
  #resolver;
132
135
  /** @type {Map<string, { promise: Promise<string>; timer: ReturnType<typeof setTimeout>; waiters: number }>} */
133
136
  #bases = new Map();
134
- /** @type {string} */
137
+ /** @type {string | undefined} */
135
138
  #token;
136
139
  /** @type {FetchLike} */
137
140
  #fetch;
138
141
  /** @type {number} */
139
142
  #maxResponseBytes;
140
143
 
141
- /** @param {TerraBaseV1ClientConfig} config */
144
+ /** @param {TerraBaseV1ClientConfig | (import("./system.js").TerraBaseSystemClientConfig & { project?: undefined; endpointResolver?: undefined })} config */
142
145
  constructor(config) {
143
146
  if (typeof config !== "object" || config === null) throw new TypeError("TerraBase v1 client config is required.");
144
- this.#project = requireProject(config.project);
147
+ this.#project = config.project === undefined ? undefined : requireProject(config.project);
145
148
  if ((config.baseUrl === undefined) === (config.endpointResolver === undefined))
146
149
  throw new TypeError("Supply exactly one of baseUrl or endpointResolver.");
147
150
  if (config.endpointResolver !== undefined && typeof config.endpointResolver !== "function")
148
151
  throw new TypeError("endpointResolver must be a function.");
149
152
  this.#resolver = config.endpointResolver ?? staticBaseUrlResolver(/** @type {string} */ (config.baseUrl));
150
- if (typeof config.token !== "string" || !tokenPattern.test(config.token))
151
- throw new TypeError("token must be a tb_ API key with 43 base64url characters.");
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.");
152
155
  if ("autoIdempotencyKey" in config || "maxRetries" in config || "resolver" in config)
153
156
  throw new TypeError("Unsupported client configuration member.");
154
157
  const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
@@ -169,8 +172,9 @@ export class V1Transport {
169
172
  */
170
173
  async url(operation, params, signal) {
171
174
  signal?.throwIfAborted();
172
- const database = requireName("database", params.database);
173
- let base = this.#bases.get(database);
175
+ const database = v1Route(operation).pathTemplate.includes("{database}") ? requireName("database", params.database) : undefined;
176
+ const session = database ?? "";
177
+ let base = this.#bases.get(session);
174
178
  if (base === undefined) {
175
179
  const controller = new AbortController();
176
180
  const timer = setTimeout(() => controller.abort(new DOMException(
@@ -180,13 +184,13 @@ export class V1Transport {
180
184
  const promise = new Promise((resolve, reject) => {
181
185
  controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
182
186
  void Promise.resolve().then(() => this.#resolver(Object.freeze({
183
- project: this.#project, database, signal: controller.signal,
187
+ project: this.#project, ...(database === undefined ? {} : { database }), signal: controller.signal,
184
188
  }))).then(canonicalBaseUrl).then(resolve, reject).finally(() => clearTimeout(timer));
185
189
  });
186
190
  base = { promise, timer, waiters: 0 };
187
- this.#bases.set(database, base);
191
+ this.#bases.set(session, base);
188
192
  // A failed resolution is not a session; a later operation may resolve again.
189
- void promise.catch(() => { if (this.#bases.get(database) === base) this.#bases.delete(database); });
193
+ void promise.catch(() => { if (this.#bases.get(session) === base) this.#bases.delete(session); });
190
194
  }
191
195
  base.waiters++;
192
196
  if (typeof base.timer === "object") base.timer.ref();
@@ -204,7 +208,8 @@ export class V1Transport {
204
208
  });
205
209
  });
206
210
  signal?.throwIfAborted();
207
- return `${resolved}${v1RoutePath(v1Route(operation), { ...params, project: this.#project })}`;
211
+ if (v1Route(operation).pathTemplate.includes("{project}") && this.#project === undefined) throw new TypeError("This operation requires a project client.");
212
+ return `${resolved}${v1RoutePath(v1Route(operation), { ...params, ...(this.#project === undefined ? {} : { project: this.#project }) })}`;
208
213
  } finally {
209
214
  // Keep Node alive only while callers are waiting for this resolution.
210
215
  if (--base.waiters === 0 && typeof base.timer === "object") base.timer.unref();
@@ -215,9 +220,11 @@ export class V1Transport {
215
220
  * Reads a response body as UTF-8 text within the response byte bound. A
216
221
  * connection failure while reading is a BodyTransportError.
217
222
  * @param {Response} response
223
+ * @param {AbortSignal | undefined} [signal]
218
224
  * @returns {Promise<string>}
219
225
  */
220
- async readText(response) {
226
+ async readText(response, signal) {
227
+ signal?.throwIfAborted();
221
228
  const reader = response.body?.getReader();
222
229
  if (reader === undefined) return "";
223
230
  /** @type {Uint8Array[]} */
@@ -225,13 +232,16 @@ export class V1Transport {
225
232
  let size = 0;
226
233
  try {
227
234
  while (true) {
235
+ signal?.throwIfAborted();
228
236
  /** @type {ReadableStreamReadResult<Uint8Array>} */
229
237
  let part;
230
238
  try {
231
239
  part = await reader.read();
232
240
  } catch (error) {
241
+ signal?.throwIfAborted();
233
242
  throw new BodyTransportError(error);
234
243
  }
244
+ signal?.throwIfAborted();
235
245
  if (part.done) break;
236
246
  size += part.value.byteLength;
237
247
  if (size > this.#maxResponseBytes)
@@ -250,16 +260,17 @@ export class V1Transport {
250
260
  bytes.set(part, offset);
251
261
  offset += part.byteLength;
252
262
  }
253
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
263
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
254
264
  }
255
265
 
256
266
  /**
257
267
  * Parses a JSON response body exactly.
258
268
  * @param {Response} response
269
+ * @param {AbortSignal | undefined} [signal]
259
270
  * @returns {Promise<JsonValue | undefined>}
260
271
  */
261
- async readJson(response) {
262
- return V1Transport.#decodeJson(response, await this.readText(response));
272
+ async readJson(response, signal) {
273
+ return V1Transport.#decodeJson(response, await this.readText(response, signal));
263
274
  }
264
275
 
265
276
  /**
@@ -278,13 +289,15 @@ export class V1Transport {
278
289
  /**
279
290
  * Converts a non-success response to a TerraBaseV1Error.
280
291
  * @param {Response} response
292
+ * @param {AbortSignal | undefined} [signal]
281
293
  * @returns {Promise<TerraBaseV1Error>}
282
294
  */
283
- async error(response) {
295
+ async error(response, signal) {
284
296
  let text = "";
285
297
  try {
286
- text = await this.readText(response);
298
+ text = await this.readText(response, signal);
287
299
  } catch {
300
+ signal?.throwIfAborted();
288
301
  text = "";
289
302
  }
290
303
  return parseV1Error(response.status, response.headers, text);
@@ -298,27 +311,60 @@ export class V1Transport {
298
311
  * @returns {Promise<V1CallResult>}
299
312
  */
300
313
  async call(operation, params, options = {}) {
314
+ const response = await this.#send(operation, params, options);
315
+ if (response.status === 307) {
316
+ await response.body?.cancel();
317
+ return { status: response.status, headers: response.headers, value: undefined };
318
+ }
319
+ return { status: response.status, headers: response.headers, value: await this.readJson(response, options.signal) };
320
+ }
321
+
322
+ /**
323
+ * @param {"streamDatabaseChanges"} operation
324
+ * @param {Readonly<Record<string, string>>} params
325
+ * @param {V1CallOptions} [options]
326
+ * @returns {Promise<Response>}
327
+ */
328
+ async openStream(operation, params, options = {}) {
329
+ const response = await this.#send(operation, params, options);
330
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
331
+ if (mediaType !== "text/event-stream" || response.body === null) {
332
+ void response.body?.cancel().catch(() => undefined);
333
+ throw new TypeError("TerraBase changes require a text/event-stream response body.");
334
+ }
335
+ return response;
336
+ }
337
+
338
+ /** @param {V1Operation} operation @param {Readonly<Record<string, string>>} params @param {V1CallOptions} options @returns {Promise<Response>} */
339
+ async #send(operation, params, options) {
301
340
  const route = v1Route(operation);
302
- if (options.body !== undefined && new TextEncoder().encode(options.body).byteLength > maximumRequestBytes)
341
+ const bytes = typeof options.body === "string" ? new TextEncoder().encode(options.body) : options.body;
342
+ if (bytes !== undefined && bytes.byteLength > maximumRequestBytes)
303
343
  throw new TypeError(`TerraBase request bodies are limited to ${maximumRequestBytes} bytes.`);
304
344
  let url = await this.url(operation, params, options.signal);
305
345
  if (options.query !== undefined && options.query.size > 0) url += `?${options.query}`;
306
346
  const headers = new Headers(options.headers);
307
- headers.set("authorization", `Bearer ${this.#token}`);
308
- headers.set("accept", route.responseMediaType);
347
+ if (route.permission !== "anonymous" && this.#token !== undefined) headers.set("authorization", `Bearer ${this.#token}`);
348
+ if (route.responseMediaType !== "") headers.set("accept", route.responseMediaType);
309
349
  if (options.body !== undefined) headers.set("content-type", options.contentType ?? "application/json");
310
- /** @type {RequestInit} */
350
+ /** @type {RequestInit & { duplex?: "half" }} */
311
351
  const init = { method: route.method, headers, redirect: "manual" };
312
- if (options.body !== undefined) init.body = options.body;
352
+ if (options.cache !== undefined) init.cache = options.cache;
353
+ if (route.permission.endsWith(":write") && typeof process !== "undefined" && process.release?.name === "node") {
354
+ // Node fetch replays buffered or absent bodies on 421; use a one-shot stream, including DELETE.
355
+ if (operation === "uploadImportSource" && bytes !== undefined) headers.set("content-length", String(bytes.byteLength));
356
+ init.body = new ReadableStream({ start(controller) { if (bytes !== undefined && bytes.byteLength > 0) controller.enqueue(bytes); controller.close(); } });
357
+ init.duplex = "half";
358
+ } else if (options.body !== undefined) init.body = typeof options.body === "string" ? options.body : options.body.slice().buffer;
313
359
  if (options.signal !== undefined) init.signal = options.signal;
314
360
  const response = await this.#fetch(url, init);
361
+ if (options.signal?.aborted) {
362
+ void response.body?.cancel().catch(() => undefined);
363
+ options.signal.throwIfAborted();
364
+ }
315
365
  if (response.type === "opaqueredirect")
316
366
  throw new TypeError("Fresh shape bootstrap requires server fetch; obtain a handle and snapshot offset with collection.shape() on your server.");
317
- if (!route.successStatuses.includes(response.status)) throw await this.error(response);
318
- if (response.status === 307) {
319
- await response.body?.cancel();
320
- return { status: response.status, headers: response.headers, value: undefined };
321
- }
322
- return { status: response.status, headers: response.headers, value: await this.readJson(response) };
367
+ if (!route.successStatuses.includes(response.status)) throw await this.error(response, options.signal);
368
+ return response;
323
369
  }
324
370
  }
@@ -0,0 +1,101 @@
1
+ import { requireId } from "../ids.js";
2
+ import { requireName } from "./ids.js";
3
+ import { dictionary, flag, list, nullable, numeric, record, text } from "./management-decode.js";
4
+ import { members } from "./management-validation.js";
5
+ /** @import { V1Transport } from "./transport.js" */
6
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
7
+ const period = record({ from: text, to: text, granularity: text, reads: numeric, writes: numeric });
8
+ const series = list(record({ start: text, reads: numeric, writes: numeric }));
9
+ const databaseUsage = record({ databaseId: requireId, storageBytes: numeric, logicalBytes: numeric, collections: numeric,
10
+ documents: numeric, reads: numeric, writes: numeric, period, series });
11
+ const projectUsage = record({ projectId: requireId, databases: numeric, storageBytes: numeric, logicalBytes: numeric,
12
+ documents: numeric, reads: numeric, writes: numeric, period, series, items: list(databaseUsage) });
13
+ const bandwidth = record({ bytesSent: numeric, bytesReceived: numeric });
14
+ const directedBandwidth = list(record({ sourceRegion: text, destinationRegion: text, kind: text, bytesSent: numeric, bytesReceived: numeric }));
15
+ const latency = record({ p50Ms: nullable(numeric), p95Ms: nullable(numeric), p99Ms: nullable(numeric) });
16
+ const analytics = record({ projectId: requireId, range: text, from: text, to: text, bucketSeconds: numeric,
17
+ items: list(record({ databaseId: requireId, databaseName: text, region: text,
18
+ series: list(record({ start: text, requests: dictionary(numeric), operations: dictionary(numeric), errors: numeric,
19
+ latency, storageBytes: nullable(numeric), clientBandwidth: bandwidth, replicationBandwidth: bandwidth })) })) });
20
+ const replication = record({ projectId: requireId, range: text, from: text, to: text, bucketSeconds: numeric,
21
+ items: list(record({ databaseId: requireId, databaseName: text, region: text, status: text, replicatedOps: numeric,
22
+ lagMs: nullable(numeric), divergence: nullable(flag), convergenceMs: nullable(numeric), lastApplied: nullable(text),
23
+ bandwidth: directedBandwidth, series: list(record({ start: text, replicatedOps: numeric, bandwidth: directedBandwidth })) })) });
24
+ /** @typedef {ReturnType<typeof databaseUsage>} V1DatabaseUsage */
25
+ /** @typedef {ReturnType<typeof projectUsage>} V1ProjectUsage */
26
+ /** @typedef {ReturnType<typeof analytics>} V1Analytics */
27
+ /** @typedef {ReturnType<typeof replication>} V1Replication */
28
+ /** @typedef {{ readonly granularity?: "hour" | "day"; readonly from?: string; readonly to?: string }} V1UsageRequest */
29
+ /** @typedef {{ readonly range?: "1h" | "24h" | "7d" | "30d"; readonly database?: string; readonly region?: string }} V1AnalyticsRequest */
30
+ /** @param {V1UsageRequest} request */
31
+ function usageQuery(request) {
32
+ members(request, ["granularity", "from", "to"]);
33
+ const query = new URLSearchParams();
34
+ if (request.granularity !== undefined) {
35
+ if (!["hour", "day"].includes(request.granularity)) throw new TypeError("granularity must be hour or day.");
36
+ query.set("granularity", request.granularity);
37
+ }
38
+ for (const name of /** @type {const} */ (["from", "to"])) {
39
+ const value = request[name];
40
+ if (value === undefined) continue;
41
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/u.test(value) || !Number.isFinite(Date.parse(value)))
42
+ throw new TypeError("from and to must be RFC 3339 instants.");
43
+ query.set(name, value);
44
+ }
45
+ return query;
46
+ }
47
+ /** @param {V1AnalyticsRequest} request @param {boolean} project */
48
+ function analyticsQuery(request, project) {
49
+ members(request, project ? ["range", "database", "region"] : ["range", "region"]);
50
+ const query = new URLSearchParams();
51
+ if (request.range !== undefined) {
52
+ if (!["1h", "24h", "7d", "30d"].includes(request.range)) throw new TypeError("Invalid analytics range.");
53
+ query.set("range", request.range);
54
+ }
55
+ if (request.database !== undefined) query.set("database", requireId(request.database));
56
+ if (request.region !== undefined) {
57
+ if (typeof request.region !== "string" || !/^[a-z][a-z0-9-]{0,62}$/u.test(request.region)) throw new TypeError("Invalid region.");
58
+ query.set("region", request.region);
59
+ }
60
+ return query;
61
+ }
62
+
63
+ /** Project and database usage retain the producer's separate response envelopes. */
64
+ export class V1ProjectUsageClient {
65
+ /** @type {V1Transport} */
66
+ #transport;
67
+ /** @param {V1Transport} transport */
68
+ constructor(transport) { this.#transport = transport; Object.freeze(this); }
69
+ /** @param {V1UsageRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1ProjectUsage>} */
70
+ async usage(request = {}, options = {}) {
71
+ return projectUsage((await this.#transport.call("getProjectUsage", {}, { query: usageQuery(request), signal: options.signal })).value);
72
+ }
73
+ /** @param {V1AnalyticsRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Analytics>} */
74
+ async analytics(request = {}, options = {}) {
75
+ return analytics((await this.#transport.call("getProjectAnalytics", {}, { query: analyticsQuery(request, true), signal: options.signal })).value);
76
+ }
77
+ /** @param {V1AnalyticsRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Replication>} */
78
+ async replication(request = {}, options = {}) {
79
+ return replication((await this.#transport.call("getProjectReplication", {}, { query: analyticsQuery(request, true), signal: options.signal })).value);
80
+ }
81
+ }
82
+ export class V1DatabaseUsageClient {
83
+ /** @type {V1Transport} */
84
+ #transport;
85
+ /** @type {{database: string}} */
86
+ #params;
87
+ /** @param {V1Transport} transport @param {string} database */
88
+ constructor(transport, database) { this.#transport = transport; this.#params = { database: requireName("database", database) }; Object.freeze(this); }
89
+ /** @param {V1UsageRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1DatabaseUsage>} */
90
+ async usage(request = {}, options = {}) {
91
+ return databaseUsage((await this.#transport.call("getDatabaseUsage", this.#params, { query: usageQuery(request), signal: options.signal })).value);
92
+ }
93
+ /** @param {Omit<V1AnalyticsRequest, "database">} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Analytics>} */
94
+ async analytics(request = {}, options = {}) {
95
+ return analytics((await this.#transport.call("getDatabaseAnalytics", this.#params, { query: analyticsQuery(request, false), signal: options.signal })).value);
96
+ }
97
+ /** @param {Omit<V1AnalyticsRequest, "database">} [request] @param {V1RequestOptions} [options] @returns {Promise<V1Replication>} */
98
+ async replication(request = {}, options = {}) {
99
+ return replication((await this.#transport.call("getDatabaseReplication", this.#params, { query: analyticsQuery(request, false), signal: options.signal })).value);
100
+ }
101
+ }
@@ -1,9 +1,3 @@
1
- /**
2
- * @param {unknown} value
3
- * @param {string} [name]
4
- * @returns {string}
5
- */
6
- export function requireDatabaseUuid(value: unknown, name?: string): string;
7
1
  /**
8
2
  * @param {unknown} value
9
3
  * @param {string} [name]
@@ -88,6 +82,7 @@ export function encodeDatabaseCommit(scope: DatabaseScope, commit: DatabaseCommi
88
82
  readonly body: string;
89
83
  readonly effect_digest: string;
90
84
  }>;
85
+ export { requireId as requireDatabaseId } from "./ids.js";
91
86
  export type Budget = {
92
87
  nodes: number;
93
88
  };
@@ -1,17 +1,17 @@
1
1
  /**
2
- * The current TerraBase document protocol uses canonical UUID entity IDs.
2
+ * Entity identities are canonical uppercase ULIDs, including explicit local development.
3
3
  */
4
- export type CanonicalUuid = string;
4
+ export type EntityId = import("./ids.js").EntityId;
5
5
  /**
6
6
  * All 64-bit quantities stay bigint in JavaScript and decimal strings on the wire.
7
7
  */
8
8
  export type DatabaseScope = {
9
- readonly tenant: CanonicalUuid;
10
- readonly database: CanonicalUuid;
9
+ readonly tenant: EntityId;
10
+ readonly database: EntityId;
11
11
  readonly shard: bigint;
12
12
  };
13
13
  export type CollectionBinding = {
14
- readonly collection: CanonicalUuid;
14
+ readonly collection: EntityId;
15
15
  readonly schema_generation: bigint;
16
16
  readonly schema_digest: string;
17
17
  };
@@ -44,12 +44,12 @@ export type DatabaseField = {
44
44
  readonly value: DatabaseValue;
45
45
  };
46
46
  export type DatabaseObserved = {
47
- readonly replica: CanonicalUuid;
47
+ readonly replica: EntityId;
48
48
  readonly incarnation: string;
49
49
  readonly sequence: bigint;
50
50
  };
51
51
  export type DatabaseEvent = DatabaseObserved & {
52
- readonly database: CanonicalUuid;
52
+ readonly database: EntityId;
53
53
  readonly shard: bigint;
54
54
  };
55
55
  export type DatabaseAuthority = ({
@@ -58,7 +58,7 @@ export type DatabaseAuthority = ({
58
58
  readonly mode: "single_writer";
59
59
  readonly generation: bigint;
60
60
  readonly writer: {
61
- readonly replica: CanonicalUuid;
61
+ readonly replica: EntityId;
62
62
  readonly incarnation: string;
63
63
  };
64
64
  });
@@ -77,7 +77,7 @@ export type DatabaseWriteValue = ({
77
77
  readonly kind: "tombstone";
78
78
  });
79
79
  export type DatabaseWrite = CollectionBinding & {
80
- readonly document: CanonicalUuid;
80
+ readonly document: EntityId;
81
81
  readonly condition: DatabaseCondition;
82
82
  readonly observed: readonly DatabaseObserved[];
83
83
  readonly value: DatabaseWriteValue;
@@ -121,7 +121,7 @@ export type DatabaseQuery = {
121
121
  readonly cursor?: string;
122
122
  };
123
123
  export type DatabaseQueryRow = {
124
- readonly document: CanonicalUuid;
124
+ readonly document: EntityId;
125
125
  readonly point: DatabasePoint;
126
126
  };
127
127
  export type DatabaseQueryPage = {
@@ -188,7 +188,7 @@ export type DatabaseSubscriptionRequest = ({
188
188
  readonly poll_cursor: string;
189
189
  });
190
190
  export type DatabaseSubscriptionEffect = {
191
- readonly document: CanonicalUuid;
191
+ readonly document: EntityId;
192
192
  readonly point: DatabasePoint | null;
193
193
  };
194
194
  export type DatabaseSubscriptionGroup = {
package/types/http.d.ts CHANGED
@@ -1,12 +1,7 @@
1
1
  /** Private transport for the named hosted identity and OIDC operations. */
2
2
  export class TsAuthHttp {
3
- /**
4
- * @param {TsAuthRuntimeConfig} config
5
- * @param {{ readonly authReverseProxy?: boolean }} [options]
6
- */
7
- constructor(config: TsAuthRuntimeConfig, options?: {
8
- readonly authReverseProxy?: boolean;
9
- });
3
+ /** @param {TsAuthRuntimeConfig} config */
4
+ constructor(config: TsAuthRuntimeConfig);
10
5
  /**
11
6
  * @readonly
12
7
  * @type {ResolvedTsAuthRuntimeConfig}
@@ -39,7 +34,6 @@ export type TsAuthRequest<TBody> = {
39
34
  readonly formBody?: URLSearchParams | string | undefined;
40
35
  readonly userinfoAccessToken?: string | undefined;
41
36
  readonly resourceIntrospectionBasic?: string | undefined;
42
- readonly issuer?: string | undefined;
43
37
  readonly signal?: AbortSignal | undefined;
44
38
  };
45
39
  import type { ResolvedTsAuthRuntimeConfig } from "./config.js";
package/types/ids.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** @returns {EntityId} Monotonic within a millisecond, including clock rollback. */
2
+ export function generateId(): EntityId;
3
+ /** @param {unknown} value @returns {value is EntityId} */
4
+ export function isValidId(value: unknown): value is EntityId;
5
+ /** @param {unknown} value @param {string} [name] @returns {EntityId} */
6
+ export function requireId(value: unknown, name?: string): EntityId;
7
+ /**
8
+ * A plain 26-character, uppercase Crockford ULID.
9
+ */
10
+ export type EntityId = string;
package/types/index.d.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  export { v1Filter } from "./v1/filter.js";
2
- export { documentIdFor } from "./v1/ids.js";
2
+ export { V1ShapeSubscription } from "./v1/shape-subscription.js";
3
3
  export { ExactDecimal } from "./v1/json.js";
4
4
  export { staticBaseUrlResolver } from "./v1/transport.js";
5
+ export { V1Databases } from "./v1/catalog.js";
6
+ export { V1SavedQueries } from "./v1/saved-queries.js";
7
+ export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
8
+ export { V1Indexes } from "./v1/indexes.js";
9
+ export { V1Imports } from "./v1/imports.js";
10
+ export type V1ShapeSubscribeOptions = import("./v1/shape-subscription.js").V1ShapeSubscribeOptions;
11
+ export type V1ShapeDocument = import("./v1/shape-subscription.js").V1ShapeDocument;
12
+ export type V1SealedShapePosition = import("./v1/shape-reader.js").V1SealedShapePosition;
13
+ export type EntityId = import("./ids.js").EntityId;
5
14
  export type TerraBaseV1ClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
6
15
  export type V1RequestOptions = import("./v1/terrabase-v1.js").V1RequestOptions;
7
16
  export type V1ReadOptions = import("./v1/terrabase-v1.js").V1ReadOptions;
@@ -31,6 +40,9 @@ export type V1JsonInputObject = import("./v1/json.js").JsonInputObject;
31
40
  export type V1ShapeRequest = import("./v1/shapes.js").V1ShapeRequest;
32
41
  export type V1ShapeChange = import("./v1/shapes.js").V1ShapeChange;
33
42
  export type V1ShapeOp = import("./v1/shapes.js").V1ShapeOp;
43
+ export type V1SavedQuery = import("./v1/saved-queries.js").V1SavedQuery;
44
+ export type V1SavedQueryRequest = import("./v1/saved-queries.js").V1SavedQueryRequest;
45
+ export type V1SavedQueryPage = import("./v1/saved-queries.js").V1SavedQueryPage;
34
46
  export type TerraBaseClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
35
47
  export type V1EndpointContext = import("./v1/transport.js").V1EndpointContext;
36
48
  export type V1EndpointResolver = import("./v1/transport.js").V1EndpointResolver;
@@ -38,6 +50,63 @@ export type V1CollectionInfo = import("./v1/terrabase-v1.js").V1CollectionInfo;
38
50
  export type V1CollectionPage = import("./v1/terrabase-v1.js").V1CollectionPage;
39
51
  export type V1CollectionListOptions = import("./v1/terrabase-v1.js").V1CollectionListOptions;
40
52
  export type V1ShapePage = import("./v1/shapes.js").V1ShapePage;
53
+ export type V1SqlRow = import("./v1/sql.js").V1SqlRow;
54
+ export type V1SqlResult = import("./v1/sql.js").V1SqlResult;
55
+ export type V1SqlIndexSuggestion = import("./v1/sql.js").V1SqlIndexSuggestion;
56
+ export type V1SqlCheck = import("./v1/sql.js").V1SqlCheck;
57
+ export type V1ChangeStreamRequest = import("./v1/changes.js").V1ChangeStreamRequest;
58
+ export type V1DatabaseChange = import("./v1/changes.js").V1DatabaseChange;
59
+ export type V1DatabaseChangeCursor = import("./v1/changes.js").V1DatabaseChangeCursor;
60
+ export type V1ManagedDatabase = import("./v1/catalog.js").V1ManagedDatabase;
61
+ export type V1DatabaseCreateRequest = import("./v1/catalog.js").V1DatabaseCreateRequest;
62
+ export type V1DatabaseUpdateRequest = import("./v1/catalog.js").V1DatabaseUpdateRequest;
63
+ export type V1DatabaseBranchRequest = import("./v1/catalog.js").V1DatabaseBranchRequest;
64
+ export type V1DatabaseListRequest = import("./v1/catalog.js").V1DatabaseListRequest;
65
+ export type V1DatabasePage = import("./v1/catalog.js").V1DatabasePage;
66
+ export type V1CollectionSchema = import("./v1/catalog.js").V1CollectionSchema;
67
+ export type V1InferredSchema = import("./v1/database-schema.js").V1InferredSchema;
68
+ export type V1DatabaseSchema = import("./v1/database-schema.js").V1DatabaseSchema;
69
+ export type V1UniqueSchema = import("./v1/database-schema.js").V1UniqueSchema;
70
+ export type V1UniqueSchemaRequest = import("./v1/database-schema.js").V1UniqueSchemaRequest;
71
+ export type V1QueryShapes = import("./v1/database-schema.js").V1QueryShapes;
72
+ export type V1Index = import("./v1/indexes.js").V1Index;
73
+ export type V1IndexList = import("./v1/indexes.js").V1IndexList;
74
+ export type V1AutoIndexPolicy = import("./v1/indexes.js").V1AutoIndexPolicy;
75
+ export type V1IndexRecommendations = import("./v1/indexes.js").V1IndexRecommendations;
76
+ export type V1AppliedIndexRecommendation = import("./v1/indexes.js").V1AppliedIndexRecommendation;
77
+ export type V1CreateIndexRequest = import("./v1/indexes.js").V1CreateIndexRequest;
78
+ export type V1DatabaseUsage = import("./v1/usage.js").V1DatabaseUsage;
79
+ export type V1ProjectUsage = import("./v1/usage.js").V1ProjectUsage;
80
+ export type V1Analytics = import("./v1/usage.js").V1Analytics;
81
+ export type V1Replication = import("./v1/usage.js").V1Replication;
82
+ export type V1UsageRequest = import("./v1/usage.js").V1UsageRequest;
83
+ export type V1AnalyticsRequest = import("./v1/usage.js").V1AnalyticsRequest;
84
+ export type V1Backup = import("./v1/backups.js").V1Backup;
85
+ export type V1BackupPage = import("./v1/backups.js").V1BackupPage;
86
+ export type V1BackupPolicy = import("./v1/backups.js").V1BackupPolicy;
87
+ export type V1RestoreWindow = import("./v1/backups.js").V1RestoreWindow;
88
+ export type V1PointInTimePlan = import("./v1/backups.js").V1PointInTimePlan;
89
+ export type V1RestoredDatabase = import("./v1/backups.js").V1RestoredDatabase;
90
+ export type V1BackupDestination = import("./v1/backups.js").V1BackupDestination;
91
+ export type V1BackupPolicyRequest = import("./v1/backups.js").V1BackupPolicyRequest;
92
+ export type V1PointInTimeRequest = import("./v1/backups.js").V1PointInTimeRequest;
93
+ export type V1PointInTimeRestoreRequest = import("./v1/backups.js").V1PointInTimeRestoreRequest;
94
+ export type V1BackupDestinationRequest = import("./v1/backups.js").V1BackupDestinationRequest;
95
+ export type V1ImportJob = import("./v1/imports.js").V1ImportJob;
96
+ export type V1ImportOptions = import("./v1/imports.js").V1ImportOptions;
97
+ export type V1CreateImportRequest = import("./v1/imports.js").V1CreateImportRequest;
98
+ export type TerraBaseSystemClientConfig = import("./v1/system.js").TerraBaseSystemClientConfig;
99
+ export type V1ProbeStatus = import("./v1/system.js").V1ProbeStatus;
100
+ export type V1ReplicationHealth = import("./v1/system.js").V1ReplicationHealth;
101
+ export type V1PlatformDatabases = import("./v1/system.js").V1PlatformDatabases;
102
+ export type V1ShapeManifest = import("./v1/shape-manifest.js").V1ShapeManifest;
103
+ export type V1ShapeManifestRequest = import("./v1/shape-manifest.js").V1ShapeManifestRequest;
104
+ export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest;
105
+ export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
41
106
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
42
107
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
43
108
  export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
109
+ export { generateId, isValidId, requireId } from "./ids.js";
110
+ export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
111
+ export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
112
+ export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.js";
@@ -3,7 +3,7 @@
3
3
  * @returns {void}
4
4
  */
5
5
  export function assertLocalSqliteEnvironment(environment?: Readonly<Record<string, string | undefined>>): void;
6
- export const localSqlitePresenceIndicators: readonly ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "FORGEJO_ACTIONS", "GITEA_ACTIONS", "TF_BUILD", "BUILDKITE", "CIRCLECI", "JENKINS_URL", "TEAMCITY_VERSION", "VERCEL", "NETLIFY", "CF_PAGES", "RENDER", "FLY_APP_NAME", "K_SERVICE", "CLOUD_RUN_JOB", "KUBERNETES_SERVICE_HOST", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV", "ECS_CONTAINER_METADATA_URI", "ECS_CONTAINER_METADATA_URI_V4", "WEBSITE_INSTANCE_ID", "WEBSITE_SITE_NAME", "FUNCTIONS_WORKER_RUNTIME", "DYNO", "HEROKU_APP_NAME", "RAILWAY_ENVIRONMENT", "RAILWAY_SERVICE_ID", "NOMAD_ALLOC_ID", "TERRASCALE_DEPLOYMENT", "TERRASCALE_QUALIFICATION"];
6
+ export const localSqlitePresenceIndicators: readonly ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "FORGEJO_ACTIONS", "GITEA_ACTIONS", "TF_BUILD", "BUILDKITE", "CIRCLECI", "JENKINS_URL", "TEAMCITY_VERSION", "VERCEL", "NETLIFY", "CF_PAGES", "RENDER", "K_SERVICE", "CLOUD_RUN_JOB", "KUBERNETES_SERVICE_HOST", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV", "ECS_CONTAINER_METADATA_URI", "ECS_CONTAINER_METADATA_URI_V4", "WEBSITE_INSTANCE_ID", "WEBSITE_SITE_NAME", "FUNCTIONS_WORKER_RUNTIME", "DYNO", "HEROKU_APP_NAME", "RAILWAY_ENVIRONMENT", "RAILWAY_SERVICE_ID", "NOMAD_ALLOC_ID", "TERRASCALE_DEPLOYMENT", "TERRASCALE_QUALIFICATION"];
7
7
  /**
8
8
  * Explicit opt-in for a hosted development environment.
9
9
  *
@@ -34,6 +34,16 @@ export class TerraScaleSqliteStore {
34
34
  * @returns {T[]}
35
35
  */
36
36
  list<T>(namespace: string, scope: string): T[];
37
+ /**
38
+ * @template T
39
+ * @param {string} namespace
40
+ * @param {string} scope
41
+ * @returns {readonly { readonly id: string; readonly value: T }[]}
42
+ */
43
+ listEntries<T>(namespace: string, scope: string): readonly {
44
+ readonly id: string;
45
+ readonly value: T;
46
+ }[];
37
47
  /**
38
48
  * @template T
39
49
  * @param {string} namespace
@@ -6,9 +6,9 @@
6
6
  export function localFixture(options?: Omit<TerraScaleSqliteRuntimeOptions, "sqlite">): Promise<{
7
7
  runtime: import("./index.js").TerraScaleSqliteRuntime;
8
8
  client: import("./legacy-client.js").TerraBaseClient;
9
- collection: `${string}-${string}-${string}-${string}-${string}`;
9
+ collection: string;
10
10
  binding: {
11
- collection: `${string}-${string}-${string}-${string}-${string}`;
11
+ collection: string;
12
12
  schema_generation: bigint;
13
13
  schema_digest: string;
14
14
  };
@@ -25,8 +25,8 @@ export function postgresQuery(statement: string, parameters?: readonly unknown[]
25
25
  * readonly port: number;
26
26
  * readonly tlsServerName: string;
27
27
  * readonly caCertificatePem: string;
28
- * readonly database: string;
29
- * readonly keyId: string;
28
+ * readonly database: import("./ids.js").EntityId;
29
+ * readonly keyId: import("./ids.js").EntityId;
30
30
  * readonly password: string;
31
31
  * readonly connectTimeoutMs?: number;
32
32
  * }} PostgresConfig
@@ -107,8 +107,8 @@ export type PostgresConfig = {
107
107
  readonly port: number;
108
108
  readonly tlsServerName: string;
109
109
  readonly caCertificatePem: string;
110
- readonly database: string;
111
- readonly keyId: string;
110
+ readonly database: import("./ids.js").EntityId;
111
+ readonly keyId: import("./ids.js").EntityId;
112
112
  readonly password: string;
113
113
  readonly connectTimeoutMs?: number;
114
114
  };