@terrascale/sdk 0.0.3 → 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 +31 -0
- package/README.md +206 -67
- package/package.json +10 -4
- package/sdk-public-v1-route-manifest.json +85 -0
- package/sdk-v1-route-manifest.json +896 -15
- package/src/http.js +3 -48
- package/src/index.js +80 -1
- package/src/local/environment.js +0 -1
- package/src/ts-auth.js +1 -5
- package/src/v1/backups.js +111 -0
- package/src/v1/catalog.js +240 -0
- package/src/v1/changes.js +225 -0
- package/src/v1/database-schema.js +45 -0
- package/src/v1/decode.js +6 -5
- package/src/v1/errors.js +23 -3
- package/src/v1/export.js +68 -0
- package/src/v1/ids.js +5 -2
- package/src/v1/imports.js +72 -0
- package/src/v1/indexes.js +76 -0
- package/src/v1/json.js +4 -3
- package/src/v1/management-decode.js +24 -0
- package/src/v1/management-routes.js +72 -0
- package/src/v1/management-validation.js +25 -0
- package/src/v1/routes.js +32 -7
- package/src/v1/saved-queries.js +58 -0
- package/src/v1/shape-crypto.js +125 -0
- package/src/v1/shape-manifest.js +31 -0
- package/src/v1/shape-reader.js +214 -0
- package/src/v1/shape-subscription.js +208 -0
- package/src/v1/shapes.js +11 -5
- package/src/v1/sql.js +98 -0
- package/src/v1/system.js +80 -0
- package/src/v1/terrabase-v1.js +231 -30
- package/src/v1/transport.js +117 -33
- package/src/v1/usage.js +101 -0
- package/types/http.d.ts +2 -8
- package/types/index.d.ts +73 -0
- package/types/local/environment.d.ts +1 -1
- package/types/ts-auth.d.ts +0 -2
- package/types/v1/backups.d.ts +128 -0
- package/types/v1/catalog.d.ts +104 -0
- package/types/v1/changes.d.ts +53 -0
- package/types/v1/database-schema.d.ts +82 -0
- package/types/v1/decode.d.ts +20 -4
- package/types/v1/errors.d.ts +16 -1
- package/types/v1/export.d.ts +26 -0
- package/types/v1/ids.d.ts +2 -2
- package/types/v1/imports.d.ts +132 -0
- package/types/v1/indexes.d.ts +75 -0
- package/types/v1/management-decode.d.ts +15 -0
- package/types/v1/management-routes.d.ts +3 -0
- package/types/v1/management-validation.d.ts +8 -0
- package/types/v1/routes.d.ts +5 -2
- package/types/v1/saved-queries.d.ts +33 -0
- package/types/v1/shape-crypto.d.ts +39 -0
- package/types/v1/shape-manifest.d.ts +28 -0
- package/types/v1/shape-reader.d.ts +45 -0
- package/types/v1/shape-subscription.d.ts +35 -0
- package/types/v1/shapes.d.ts +16 -2
- package/types/v1/sql.d.ts +47 -0
- package/types/v1/system.d.ts +85 -0
- package/types/v1/terrabase-v1.d.ts +140 -6
- package/types/v1/transport.d.ts +33 -10
- package/types/v1/usage.d.ts +171 -0
- package/src/ts-auth-discovery.js +0 -207
- package/types/ts-auth-discovery.d.ts +0 -19
package/src/v1/transport.js
CHANGED
|
@@ -16,19 +16,20 @@ 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
18
|
// Validate header-safe credential syntax only; the producer validates JWT claims/signatures.
|
|
19
|
-
// Retain C# keys
|
|
19
|
+
// Retain C# keys; current project routes also authenticate project access JWTs.
|
|
20
20
|
const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|tb_[A-Za-z0-9_-]{43})$/u;
|
|
21
21
|
/**
|
|
22
22
|
* Resolver input contains no credential. A successful URL is cached for this client/database.
|
|
23
|
-
* @typedef {{ readonly project: string; readonly database
|
|
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]
|
|
@@ -37,11 +38,12 @@ const tokenPattern = /^(?!tbk1\.)(?:[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-
|
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* @typedef {object} V1CallOptions
|
|
40
|
-
* @property {string} [body]
|
|
41
|
+
* @property {string | Uint8Array} [body]
|
|
41
42
|
* @property {string} [contentType]
|
|
42
43
|
* @property {Readonly<Record<string, string>>} [headers] Contract headers only (If-Match, If-None-Match).
|
|
43
44
|
* @property {URLSearchParams | undefined} [query]
|
|
44
45
|
* @property {AbortSignal | undefined} [signal]
|
|
46
|
+
* @property {"no-store" | undefined} [cache] Authenticated shape epoch keys must bypass caches.
|
|
45
47
|
*/
|
|
46
48
|
|
|
47
49
|
/**
|
|
@@ -116,6 +118,13 @@ function canonicalBaseUrl(raw) {
|
|
|
116
118
|
return `${url.origin}${url.pathname.replace(/\/+$/u, "")}`;
|
|
117
119
|
}
|
|
118
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
|
+
|
|
119
128
|
/** A response body that failed in transit after its headers arrived. */
|
|
120
129
|
export class BodyTransportError extends Error {
|
|
121
130
|
/** @param {unknown} cause */
|
|
@@ -127,30 +136,30 @@ export class BodyTransportError extends Error {
|
|
|
127
136
|
|
|
128
137
|
/** Internal transport bound to one project credential. */
|
|
129
138
|
export class V1Transport {
|
|
130
|
-
/** @type {string} */
|
|
139
|
+
/** @type {string | undefined} */
|
|
131
140
|
#project;
|
|
132
141
|
/** @type {V1EndpointResolver} */
|
|
133
142
|
#resolver;
|
|
134
143
|
/** @type {Map<string, { promise: Promise<string>; timer: ReturnType<typeof setTimeout>; waiters: number }>} */
|
|
135
144
|
#bases = new Map();
|
|
136
|
-
/** @type {string} */
|
|
145
|
+
/** @type {string | V1TokenProvider | undefined} */
|
|
137
146
|
#token;
|
|
138
147
|
/** @type {FetchLike} */
|
|
139
148
|
#fetch;
|
|
140
149
|
/** @type {number} */
|
|
141
150
|
#maxResponseBytes;
|
|
142
151
|
|
|
143
|
-
/** @param {TerraBaseV1ClientConfig} config */
|
|
152
|
+
/** @param {TerraBaseV1ClientConfig | (import("./system.js").TerraBaseSystemClientConfig & { project?: undefined; endpointResolver?: undefined })} config */
|
|
144
153
|
constructor(config) {
|
|
145
154
|
if (typeof config !== "object" || config === null) throw new TypeError("TerraBase v1 client config is required.");
|
|
146
|
-
this.#project = requireProject(config.project);
|
|
155
|
+
this.#project = config.project === undefined ? undefined : requireProject(config.project);
|
|
147
156
|
if ((config.baseUrl === undefined) === (config.endpointResolver === undefined))
|
|
148
157
|
throw new TypeError("Supply exactly one of baseUrl or endpointResolver.");
|
|
149
158
|
if (config.endpointResolver !== undefined && typeof config.endpointResolver !== "function")
|
|
150
159
|
throw new TypeError("endpointResolver must be a function.");
|
|
151
160
|
this.#resolver = config.endpointResolver ?? staticBaseUrlResolver(/** @type {string} */ (config.baseUrl));
|
|
152
|
-
if (
|
|
153
|
-
|
|
161
|
+
if ((config.project !== undefined || config.token !== undefined) && typeof config.token !== "function")
|
|
162
|
+
requireToken(config.token);
|
|
154
163
|
if ("autoIdempotencyKey" in config || "maxRetries" in config || "resolver" in config)
|
|
155
164
|
throw new TypeError("Unsupported client configuration member.");
|
|
156
165
|
const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -163,6 +172,35 @@ export class V1Transport {
|
|
|
163
172
|
this.#maxResponseBytes = maxResponseBytes;
|
|
164
173
|
}
|
|
165
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
|
+
|
|
166
204
|
/**
|
|
167
205
|
* @param {V1Operation} operation
|
|
168
206
|
* @param {Readonly<Record<string, string>>} params
|
|
@@ -171,8 +209,9 @@ export class V1Transport {
|
|
|
171
209
|
*/
|
|
172
210
|
async url(operation, params, signal) {
|
|
173
211
|
signal?.throwIfAborted();
|
|
174
|
-
const database = requireName("database", params.database);
|
|
175
|
-
|
|
212
|
+
const database = v1Route(operation).pathTemplate.includes("{database}") ? requireName("database", params.database) : undefined;
|
|
213
|
+
const session = database ?? "";
|
|
214
|
+
let base = this.#bases.get(session);
|
|
176
215
|
if (base === undefined) {
|
|
177
216
|
const controller = new AbortController();
|
|
178
217
|
const timer = setTimeout(() => controller.abort(new DOMException(
|
|
@@ -182,13 +221,13 @@ export class V1Transport {
|
|
|
182
221
|
const promise = new Promise((resolve, reject) => {
|
|
183
222
|
controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
|
|
184
223
|
void Promise.resolve().then(() => this.#resolver(Object.freeze({
|
|
185
|
-
project: this.#project, database, signal: controller.signal,
|
|
224
|
+
project: this.#project, ...(database === undefined ? {} : { database }), signal: controller.signal,
|
|
186
225
|
}))).then(canonicalBaseUrl).then(resolve, reject).finally(() => clearTimeout(timer));
|
|
187
226
|
});
|
|
188
227
|
base = { promise, timer, waiters: 0 };
|
|
189
|
-
this.#bases.set(
|
|
228
|
+
this.#bases.set(session, base);
|
|
190
229
|
// A failed resolution is not a session; a later operation may resolve again.
|
|
191
|
-
void promise.catch(() => { if (this.#bases.get(
|
|
230
|
+
void promise.catch(() => { if (this.#bases.get(session) === base) this.#bases.delete(session); });
|
|
192
231
|
}
|
|
193
232
|
base.waiters++;
|
|
194
233
|
if (typeof base.timer === "object") base.timer.ref();
|
|
@@ -206,7 +245,8 @@ export class V1Transport {
|
|
|
206
245
|
});
|
|
207
246
|
});
|
|
208
247
|
signal?.throwIfAborted();
|
|
209
|
-
|
|
248
|
+
if (v1Route(operation).pathTemplate.includes("{project}") && this.#project === undefined) throw new TypeError("This operation requires a project client.");
|
|
249
|
+
return `${resolved}${v1RoutePath(v1Route(operation), { ...params, ...(this.#project === undefined ? {} : { project: this.#project }) })}`;
|
|
210
250
|
} finally {
|
|
211
251
|
// Keep Node alive only while callers are waiting for this resolution.
|
|
212
252
|
if (--base.waiters === 0 && typeof base.timer === "object") base.timer.unref();
|
|
@@ -217,9 +257,11 @@ export class V1Transport {
|
|
|
217
257
|
* Reads a response body as UTF-8 text within the response byte bound. A
|
|
218
258
|
* connection failure while reading is a BodyTransportError.
|
|
219
259
|
* @param {Response} response
|
|
260
|
+
* @param {AbortSignal | undefined} [signal]
|
|
220
261
|
* @returns {Promise<string>}
|
|
221
262
|
*/
|
|
222
|
-
async readText(response) {
|
|
263
|
+
async readText(response, signal) {
|
|
264
|
+
signal?.throwIfAborted();
|
|
223
265
|
const reader = response.body?.getReader();
|
|
224
266
|
if (reader === undefined) return "";
|
|
225
267
|
/** @type {Uint8Array[]} */
|
|
@@ -227,13 +269,16 @@ export class V1Transport {
|
|
|
227
269
|
let size = 0;
|
|
228
270
|
try {
|
|
229
271
|
while (true) {
|
|
272
|
+
signal?.throwIfAborted();
|
|
230
273
|
/** @type {ReadableStreamReadResult<Uint8Array>} */
|
|
231
274
|
let part;
|
|
232
275
|
try {
|
|
233
276
|
part = await reader.read();
|
|
234
277
|
} catch (error) {
|
|
278
|
+
signal?.throwIfAborted();
|
|
235
279
|
throw new BodyTransportError(error);
|
|
236
280
|
}
|
|
281
|
+
signal?.throwIfAborted();
|
|
237
282
|
if (part.done) break;
|
|
238
283
|
size += part.value.byteLength;
|
|
239
284
|
if (size > this.#maxResponseBytes)
|
|
@@ -252,16 +297,17 @@ export class V1Transport {
|
|
|
252
297
|
bytes.set(part, offset);
|
|
253
298
|
offset += part.byteLength;
|
|
254
299
|
}
|
|
255
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
300
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
256
301
|
}
|
|
257
302
|
|
|
258
303
|
/**
|
|
259
304
|
* Parses a JSON response body exactly.
|
|
260
305
|
* @param {Response} response
|
|
306
|
+
* @param {AbortSignal | undefined} [signal]
|
|
261
307
|
* @returns {Promise<JsonValue | undefined>}
|
|
262
308
|
*/
|
|
263
|
-
async readJson(response) {
|
|
264
|
-
return V1Transport.#decodeJson(response, await this.readText(response));
|
|
309
|
+
async readJson(response, signal) {
|
|
310
|
+
return V1Transport.#decodeJson(response, await this.readText(response, signal));
|
|
265
311
|
}
|
|
266
312
|
|
|
267
313
|
/**
|
|
@@ -280,13 +326,15 @@ export class V1Transport {
|
|
|
280
326
|
/**
|
|
281
327
|
* Converts a non-success response to a TerraBaseV1Error.
|
|
282
328
|
* @param {Response} response
|
|
329
|
+
* @param {AbortSignal | undefined} [signal]
|
|
283
330
|
* @returns {Promise<TerraBaseV1Error>}
|
|
284
331
|
*/
|
|
285
|
-
async error(response) {
|
|
332
|
+
async error(response, signal) {
|
|
286
333
|
let text = "";
|
|
287
334
|
try {
|
|
288
|
-
text = await this.readText(response);
|
|
335
|
+
text = await this.readText(response, signal);
|
|
289
336
|
} catch {
|
|
337
|
+
signal?.throwIfAborted();
|
|
290
338
|
text = "";
|
|
291
339
|
}
|
|
292
340
|
return parseV1Error(response.status, response.headers, text);
|
|
@@ -300,27 +348,63 @@ export class V1Transport {
|
|
|
300
348
|
* @returns {Promise<V1CallResult>}
|
|
301
349
|
*/
|
|
302
350
|
async call(operation, params, options = {}) {
|
|
351
|
+
const response = await this.#send(operation, params, options);
|
|
352
|
+
if (response.status === 307) {
|
|
353
|
+
await response.body?.cancel();
|
|
354
|
+
return { status: response.status, headers: response.headers, value: undefined };
|
|
355
|
+
}
|
|
356
|
+
return { status: response.status, headers: response.headers, value: await this.readJson(response, options.signal) };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {"streamDatabaseChanges" | "exportDatabase"} operation
|
|
361
|
+
* @param {Readonly<Record<string, string>>} params
|
|
362
|
+
* @param {V1CallOptions} [options]
|
|
363
|
+
* @returns {Promise<Response>}
|
|
364
|
+
*/
|
|
365
|
+
async openStream(operation, params, options = {}) {
|
|
366
|
+
const response = await this.#send(operation, params, options);
|
|
367
|
+
const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
368
|
+
const expected = v1Route(operation).responseMediaType;
|
|
369
|
+
if (mediaType !== expected || response.body === null) {
|
|
370
|
+
void response.body?.cancel().catch(() => undefined);
|
|
371
|
+
throw new TypeError(`TerraBase ${operation} requires a ${expected} response body.`);
|
|
372
|
+
}
|
|
373
|
+
return response;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** @param {V1Operation} operation @param {Readonly<Record<string, string>>} params @param {V1CallOptions} options @returns {Promise<Response>} */
|
|
377
|
+
async #send(operation, params, options) {
|
|
303
378
|
const route = v1Route(operation);
|
|
304
|
-
|
|
379
|
+
const bytes = typeof options.body === "string" ? new TextEncoder().encode(options.body) : options.body;
|
|
380
|
+
if (bytes !== undefined && bytes.byteLength > maximumRequestBytes)
|
|
305
381
|
throw new TypeError(`TerraBase request bodies are limited to ${maximumRequestBytes} bytes.`);
|
|
306
382
|
let url = await this.url(operation, params, options.signal);
|
|
307
383
|
if (options.query !== undefined && options.query.size > 0) url += `?${options.query}`;
|
|
308
384
|
const headers = new Headers(options.headers);
|
|
309
|
-
|
|
310
|
-
|
|
385
|
+
if (route.permission !== "anonymous" && this.#token !== undefined)
|
|
386
|
+
headers.set("authorization", `Bearer ${await this.#accessToken(options.signal)}`);
|
|
387
|
+
if (route.responseMediaType !== "") headers.set("accept", route.responseMediaType);
|
|
311
388
|
if (options.body !== undefined) headers.set("content-type", options.contentType ?? "application/json");
|
|
312
|
-
/** @type {RequestInit} */
|
|
389
|
+
/** @type {RequestInit & { duplex?: "half" }} */
|
|
313
390
|
const init = { method: route.method, headers, redirect: "manual" };
|
|
314
|
-
if (options.
|
|
391
|
+
if (options.cache !== undefined) init.cache = options.cache;
|
|
392
|
+
if (route.permission.endsWith(":write") && typeof process !== "undefined" && process.release?.name === "node") {
|
|
393
|
+
// Node fetch replays buffered or absent bodies on 421; use a one-shot stream, including DELETE.
|
|
394
|
+
if (operation === "uploadImportSource" && bytes !== undefined) headers.set("content-length", String(bytes.byteLength));
|
|
395
|
+
init.body = new ReadableStream({ start(controller) { if (bytes !== undefined && bytes.byteLength > 0) controller.enqueue(bytes); controller.close(); } });
|
|
396
|
+
init.duplex = "half";
|
|
397
|
+
} else if (options.body !== undefined) init.body = typeof options.body === "string" ? options.body : options.body.slice().buffer;
|
|
315
398
|
if (options.signal !== undefined) init.signal = options.signal;
|
|
399
|
+
options.signal?.throwIfAborted();
|
|
316
400
|
const response = await this.#fetch(url, init);
|
|
401
|
+
if (options.signal?.aborted) {
|
|
402
|
+
void response.body?.cancel().catch(() => undefined);
|
|
403
|
+
options.signal.throwIfAborted();
|
|
404
|
+
}
|
|
317
405
|
if (response.type === "opaqueredirect")
|
|
318
406
|
throw new TypeError("Fresh shape bootstrap requires server fetch; obtain a handle and snapshot offset with collection.shape() on your server.");
|
|
319
|
-
if (!route.successStatuses.includes(response.status)) throw await this.error(response);
|
|
320
|
-
|
|
321
|
-
await response.body?.cancel();
|
|
322
|
-
return { status: response.status, headers: response.headers, value: undefined };
|
|
323
|
-
}
|
|
324
|
-
return { status: response.status, headers: response.headers, value: await this.readJson(response) };
|
|
407
|
+
if (!route.successStatuses.includes(response.status)) throw await this.error(response, options.signal);
|
|
408
|
+
return response;
|
|
325
409
|
}
|
|
326
410
|
}
|
package/src/v1/usage.js
ADDED
|
@@ -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
|
+
}
|
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
|
-
|
|
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/index.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
export { v1Filter } from "./v1/filter.js";
|
|
2
|
+
export { V1ShapeSubscription } from "./v1/shape-subscription.js";
|
|
2
3
|
export { ExactDecimal } from "./v1/json.js";
|
|
3
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;
|
|
4
13
|
export type EntityId = import("./ids.js").EntityId;
|
|
5
14
|
export type TerraBaseV1ClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
|
|
15
|
+
export type V1TokenProvider = import("./v1/transport.js").V1TokenProvider;
|
|
6
16
|
export type V1RequestOptions = import("./v1/terrabase-v1.js").V1RequestOptions;
|
|
7
17
|
export type V1ReadOptions = import("./v1/terrabase-v1.js").V1ReadOptions;
|
|
8
18
|
export type V1WriteOptions = import("./v1/terrabase-v1.js").V1WriteOptions;
|
|
@@ -11,6 +21,7 @@ export type V1ConditionalWriteOptions = import("./v1/terrabase-v1.js").V1Conditi
|
|
|
11
21
|
export type V1PatchOp = import("./v1/terrabase-v1.js").V1PatchOp;
|
|
12
22
|
export type V1Patch = import("./v1/terrabase-v1.js").V1Patch;
|
|
13
23
|
export type V1SortKey = import("./v1/terrabase-v1.js").V1SortKey;
|
|
24
|
+
export type V1NearSortKey = import("./v1/terrabase-v1.js").V1NearSortKey;
|
|
14
25
|
export type V1Query = import("./v1/terrabase-v1.js").V1Query;
|
|
15
26
|
export type V1Count = import("./v1/terrabase-v1.js").V1Count;
|
|
16
27
|
export type V1QueryPage = import("./v1/terrabase-v1.js").V1QueryPage;
|
|
@@ -31,6 +42,9 @@ export type V1JsonInputObject = import("./v1/json.js").JsonInputObject;
|
|
|
31
42
|
export type V1ShapeRequest = import("./v1/shapes.js").V1ShapeRequest;
|
|
32
43
|
export type V1ShapeChange = import("./v1/shapes.js").V1ShapeChange;
|
|
33
44
|
export type V1ShapeOp = import("./v1/shapes.js").V1ShapeOp;
|
|
45
|
+
export type V1SavedQuery = import("./v1/saved-queries.js").V1SavedQuery;
|
|
46
|
+
export type V1SavedQueryRequest = import("./v1/saved-queries.js").V1SavedQueryRequest;
|
|
47
|
+
export type V1SavedQueryPage = import("./v1/saved-queries.js").V1SavedQueryPage;
|
|
34
48
|
export type TerraBaseClientConfig = import("./v1/transport.js").TerraBaseV1ClientConfig;
|
|
35
49
|
export type V1EndpointContext = import("./v1/transport.js").V1EndpointContext;
|
|
36
50
|
export type V1EndpointResolver = import("./v1/transport.js").V1EndpointResolver;
|
|
@@ -38,7 +52,66 @@ export type V1CollectionInfo = import("./v1/terrabase-v1.js").V1CollectionInfo;
|
|
|
38
52
|
export type V1CollectionPage = import("./v1/terrabase-v1.js").V1CollectionPage;
|
|
39
53
|
export type V1CollectionListOptions = import("./v1/terrabase-v1.js").V1CollectionListOptions;
|
|
40
54
|
export type V1ShapePage = import("./v1/shapes.js").V1ShapePage;
|
|
55
|
+
export type V1SqlRow = import("./v1/sql.js").V1SqlRow;
|
|
56
|
+
export type V1SqlResult = import("./v1/sql.js").V1SqlResult;
|
|
57
|
+
export type V1SqlIndexSuggestion = import("./v1/sql.js").V1SqlIndexSuggestion;
|
|
58
|
+
export type V1SqlCheck = import("./v1/sql.js").V1SqlCheck;
|
|
59
|
+
export type V1ChangeStreamRequest = import("./v1/changes.js").V1ChangeStreamRequest;
|
|
60
|
+
export type V1DatabaseChange = import("./v1/changes.js").V1DatabaseChange;
|
|
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;
|
|
64
|
+
export type V1ManagedDatabase = import("./v1/catalog.js").V1ManagedDatabase;
|
|
65
|
+
export type V1DatabaseCreateRequest = import("./v1/catalog.js").V1DatabaseCreateRequest;
|
|
66
|
+
export type V1DatabaseUpdateRequest = import("./v1/catalog.js").V1DatabaseUpdateRequest;
|
|
67
|
+
export type V1DatabaseBranchRequest = import("./v1/catalog.js").V1DatabaseBranchRequest;
|
|
68
|
+
export type V1DatabaseListRequest = import("./v1/catalog.js").V1DatabaseListRequest;
|
|
69
|
+
export type V1DatabasePage = import("./v1/catalog.js").V1DatabasePage;
|
|
70
|
+
export type V1CollectionSchema = import("./v1/catalog.js").V1CollectionSchema;
|
|
71
|
+
export type V1CollectionTtl = import("./v1/catalog.js").V1CollectionTtl;
|
|
72
|
+
export type V1InferredSchema = import("./v1/database-schema.js").V1InferredSchema;
|
|
73
|
+
export type V1DatabaseSchema = import("./v1/database-schema.js").V1DatabaseSchema;
|
|
74
|
+
export type V1UniqueSchema = import("./v1/database-schema.js").V1UniqueSchema;
|
|
75
|
+
export type V1UniqueSchemaRequest = import("./v1/database-schema.js").V1UniqueSchemaRequest;
|
|
76
|
+
export type V1QueryShapes = import("./v1/database-schema.js").V1QueryShapes;
|
|
77
|
+
export type V1Index = import("./v1/indexes.js").V1Index;
|
|
78
|
+
export type V1IndexList = import("./v1/indexes.js").V1IndexList;
|
|
79
|
+
export type V1AutoIndexPolicy = import("./v1/indexes.js").V1AutoIndexPolicy;
|
|
80
|
+
export type V1IndexRecommendations = import("./v1/indexes.js").V1IndexRecommendations;
|
|
81
|
+
export type V1AppliedIndexRecommendation = import("./v1/indexes.js").V1AppliedIndexRecommendation;
|
|
82
|
+
export type V1CreateIndexRequest = import("./v1/indexes.js").V1CreateIndexRequest;
|
|
83
|
+
export type V1DatabaseUsage = import("./v1/usage.js").V1DatabaseUsage;
|
|
84
|
+
export type V1ProjectUsage = import("./v1/usage.js").V1ProjectUsage;
|
|
85
|
+
export type V1Analytics = import("./v1/usage.js").V1Analytics;
|
|
86
|
+
export type V1Replication = import("./v1/usage.js").V1Replication;
|
|
87
|
+
export type V1UsageRequest = import("./v1/usage.js").V1UsageRequest;
|
|
88
|
+
export type V1AnalyticsRequest = import("./v1/usage.js").V1AnalyticsRequest;
|
|
89
|
+
export type V1Backup = import("./v1/backups.js").V1Backup;
|
|
90
|
+
export type V1BackupPage = import("./v1/backups.js").V1BackupPage;
|
|
91
|
+
export type V1BackupPolicy = import("./v1/backups.js").V1BackupPolicy;
|
|
92
|
+
export type V1RestoreWindow = import("./v1/backups.js").V1RestoreWindow;
|
|
93
|
+
export type V1PointInTimePlan = import("./v1/backups.js").V1PointInTimePlan;
|
|
94
|
+
export type V1RestoredDatabase = import("./v1/backups.js").V1RestoredDatabase;
|
|
95
|
+
export type V1BackupDestination = import("./v1/backups.js").V1BackupDestination;
|
|
96
|
+
export type V1BackupPolicyRequest = import("./v1/backups.js").V1BackupPolicyRequest;
|
|
97
|
+
export type V1PointInTimeRequest = import("./v1/backups.js").V1PointInTimeRequest;
|
|
98
|
+
export type V1PointInTimeRestoreRequest = import("./v1/backups.js").V1PointInTimeRestoreRequest;
|
|
99
|
+
export type V1BackupDestinationRequest = import("./v1/backups.js").V1BackupDestinationRequest;
|
|
100
|
+
export type V1ImportJob = import("./v1/imports.js").V1ImportJob;
|
|
101
|
+
export type V1ImportOptions = import("./v1/imports.js").V1ImportOptions;
|
|
102
|
+
export type V1CreateImportRequest = import("./v1/imports.js").V1CreateImportRequest;
|
|
103
|
+
export type TerraBaseSystemClientConfig = import("./v1/system.js").TerraBaseSystemClientConfig;
|
|
104
|
+
export type V1ProbeStatus = import("./v1/system.js").V1ProbeStatus;
|
|
105
|
+
export type V1ReplicationHealth = import("./v1/system.js").V1ReplicationHealth;
|
|
106
|
+
export type V1PlatformDatabases = import("./v1/system.js").V1PlatformDatabases;
|
|
107
|
+
export type V1ShapeManifest = import("./v1/shape-manifest.js").V1ShapeManifest;
|
|
108
|
+
export type V1ShapeManifestRequest = import("./v1/shape-manifest.js").V1ShapeManifestRequest;
|
|
109
|
+
export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest;
|
|
110
|
+
export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
|
|
41
111
|
export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
|
|
42
112
|
export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
|
|
43
113
|
export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
|
|
44
114
|
export { generateId, isValidId, requireId } from "./ids.js";
|
|
115
|
+
export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
|
|
116
|
+
export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
|
|
117
|
+
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", "
|
|
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
|
*
|
package/types/ts-auth.d.ts
CHANGED
|
@@ -38,7 +38,6 @@ export function verifyTsAuthWebhookWithTolerance(signingSecret: string, timestam
|
|
|
38
38
|
* @typedef {Omit<TsAuthRuntimeConfig, "apiBaseUrl"> & {
|
|
39
39
|
* readonly apiBaseUrl: string; // The ts-auth deployment's public origin; there is no default.
|
|
40
40
|
* readonly instanceSlug: string; // The instance the `sk_` key belongs to; it prefixes every route.
|
|
41
|
-
* readonly useReverseProxy?: boolean; // Route public auth calls through the reverse proxy instead of regional discovery.
|
|
42
41
|
* }} TsAuthClientConfig
|
|
43
42
|
*/
|
|
44
43
|
/**
|
|
@@ -1335,7 +1334,6 @@ export type TsAuthRuntimeConfig = import("./config.js").TsAuthRuntimeConfig;
|
|
|
1335
1334
|
export type TsAuthClientConfig = Omit<TsAuthRuntimeConfig, "apiBaseUrl"> & {
|
|
1336
1335
|
readonly apiBaseUrl: string;
|
|
1337
1336
|
readonly instanceSlug: string;
|
|
1338
|
-
readonly useReverseProxy?: boolean;
|
|
1339
1337
|
};
|
|
1340
1338
|
export type TsAuthRequestOptions = {
|
|
1341
1339
|
readonly signal?: AbortSignal | undefined;
|