@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
package/src/http.js CHANGED
@@ -1,9 +1,5 @@
1
1
  import { resolveAccessToken, resolveTsAuthRuntimeConfig } from "./config.js";
2
2
  import { err, ok, sanitizeDiagnostic } from "./result.js";
3
- import {
4
- regionalIssuerOrigin,
5
- TsAuthEndpointDiscovery,
6
- } from "./ts-auth-discovery.js";
7
3
  import { parseDatabaseJson } from "./database-codec.js";
8
4
  import { validateTsAuthIds } from "./ts-auth-ids.js";
9
5
  /** @import { ResolvedTsAuthRuntimeConfig, TsAuthRuntimeConfig, FetchLike } from "./config.js" */
@@ -22,7 +18,6 @@ import { validateTsAuthIds } from "./ts-auth-ids.js";
22
18
  * readonly formBody?: URLSearchParams | string | undefined;
23
19
  * readonly userinfoAccessToken?: string | undefined;
24
20
  * readonly resourceIntrospectionBasic?: string | undefined;
25
- * readonly issuer?: string | undefined; // Behind the Atlas proxy a regional issuer is called on its own host.
26
21
  * readonly signal?: AbortSignal | undefined;
27
22
  * }} TsAuthRequest
28
23
  */
@@ -40,25 +35,14 @@ export class TsAuthHttp {
40
35
  #fetch;
41
36
  /** @readonly @type {TsAuthRuntimeConfig["accessToken"]} */
42
37
  #accessToken;
43
- /** @readonly @type {TsAuthEndpointDiscovery | undefined} */
44
- #discovery;
45
-
46
- /**
47
- * @param {TsAuthRuntimeConfig} config
48
- * @param {{ readonly authReverseProxy?: boolean }} [options]
49
- */
50
- constructor(config, options) {
38
+ /** @param {TsAuthRuntimeConfig} config */
39
+ constructor(config) {
51
40
  this.config = Object.freeze(resolveTsAuthRuntimeConfig(config));
52
41
  const fetchHandler = config.fetch ?? globalThis.fetch?.bind(globalThis);
53
42
  if (fetchHandler === undefined)
54
43
  throw new TypeError("TsAuthClient requires a fetch implementation.");
55
44
  this.#fetch = fetchHandler;
56
45
  this.#accessToken = config.accessToken;
57
- this.#discovery =
58
- options?.authReverseProxy !== true &&
59
- this.config.apiBaseUrl === "https://auth.terrascale.tech"
60
- ? new TsAuthEndpointDiscovery(fetchHandler)
61
- : undefined;
62
46
  }
63
47
 
64
48
  /**
@@ -90,18 +74,6 @@ export class TsAuthHttp {
90
74
  options.body !== undefined
91
75
  ? JSON.stringify(options.body)
92
76
  : options.formBody?.toString();
93
- const issuerOrigin =
94
- this.#discovery === undefined || options.issuer === undefined
95
- ? undefined
96
- : regionalIssuerOrigin(options.issuer);
97
- const endpoints =
98
- issuerOrigin === undefined
99
- ? await withCancellation(
100
- this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
101
- signal,
102
- )
103
- : [issuerOrigin];
104
- let endpointIndex = 0;
105
77
  let attempt = 0;
106
78
  let refreshed = false;
107
79
  /** @type {string | undefined} */
@@ -163,7 +135,7 @@ export class TsAuthHttp {
163
135
  );
164
136
  const request = new Request(
165
137
  buildUrl(
166
- endpoints?.[endpointIndex] ?? this.config.apiBaseUrl,
138
+ this.config.apiBaseUrl,
167
139
  options.path,
168
140
  options.query,
169
141
  ),
@@ -189,16 +161,6 @@ export class TsAuthHttp {
189
161
  signal.throwIfAborted();
190
162
  }
191
163
  responseReceived = true;
192
- if (
193
- options.method === "GET" &&
194
- endpoints !== undefined &&
195
- endpointIndex < endpoints.length - 1 &&
196
- [502, 503, 504].includes(response.status)
197
- ) {
198
- void response.body?.cancel().catch(() => undefined);
199
- endpointIndex += 1;
200
- continue;
201
- }
202
164
  if (
203
165
  response.type === "opaqueredirect" ||
204
166
  (response.status >= 300 && response.status < 400)
@@ -293,13 +255,6 @@ export class TsAuthHttp {
293
255
  !signal?.aborted &&
294
256
  options.method === "GET"
295
257
  ) {
296
- if (
297
- endpoints !== undefined &&
298
- endpointIndex < endpoints.length - 1
299
- ) {
300
- endpointIndex += 1;
301
- continue;
302
- }
303
258
  if (attempt < this.config.maxRetryAttempts) {
304
259
  await delayForRetry(
305
260
  retryDelay(undefined, this.config.retryDelayMs, attempt),
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient } from "./v1/terrabase-v1.js";
2
2
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
3
- // TerraBase v1: name-based documents, transactions, queries and shapes.
3
+ // Project-scoped TerraBase API; shape support is retained for the historical release.
4
4
  export {
5
5
  createTerraBaseV1Client,
6
6
  TerraBaseV1Client,
@@ -19,6 +19,10 @@ export {
19
19
  isTerraBaseV1Error,
20
20
  } from "./v1/errors.js";
21
21
  export { v1Filter } from "./v1/filter.js";
22
+ export { V1ShapeSubscription } from "./v1/shape-subscription.js";
23
+ /** @typedef {import("./v1/shape-subscription.js").V1ShapeSubscribeOptions} V1ShapeSubscribeOptions */
24
+ /** @typedef {import("./v1/shape-subscription.js").V1ShapeDocument} V1ShapeDocument */
25
+ /** @typedef {import("./v1/shape-reader.js").V1SealedShapePosition} V1SealedShapePosition */
22
26
  export { generateId, isValidId, requireId } from "./ids.js";
23
27
  /** @typedef {import("./ids.js").EntityId} EntityId */
24
28
  export { ExactDecimal } from "./v1/json.js";
@@ -54,6 +58,11 @@ export { ExactDecimal } from "./v1/json.js";
54
58
  /** @typedef {import("./v1/shapes.js").V1ShapeOp} V1ShapeOp */
55
59
 
56
60
  export { staticBaseUrlResolver } from "./v1/transport.js";
61
+ export { V1Databases } from "./v1/catalog.js";
62
+ export { V1SavedQueries } from "./v1/saved-queries.js";
63
+ /** @typedef {import("./v1/saved-queries.js").V1SavedQuery} V1SavedQuery */
64
+ /** @typedef {import("./v1/saved-queries.js").V1SavedQueryRequest} V1SavedQueryRequest */
65
+ /** @typedef {import("./v1/saved-queries.js").V1SavedQueryPage} V1SavedQueryPage */
57
66
  /** @typedef {import("./v1/transport.js").TerraBaseV1ClientConfig} TerraBaseClientConfig */
58
67
  /** @typedef {import("./v1/transport.js").V1EndpointContext} V1EndpointContext */
59
68
  /** @typedef {import("./v1/transport.js").V1EndpointResolver} V1EndpointResolver */
@@ -61,3 +70,68 @@ export { staticBaseUrlResolver } from "./v1/transport.js";
61
70
  /** @typedef {import("./v1/terrabase-v1.js").V1CollectionPage} V1CollectionPage */
62
71
  /** @typedef {import("./v1/terrabase-v1.js").V1CollectionListOptions} V1CollectionListOptions */
63
72
  /** @typedef {import("./v1/shapes.js").V1ShapePage} V1ShapePage */
73
+ /** @typedef {import("./v1/sql.js").V1SqlRow} V1SqlRow */
74
+ /** @typedef {import("./v1/sql.js").V1SqlResult} V1SqlResult */
75
+ /** @typedef {import("./v1/sql.js").V1SqlIndexSuggestion} V1SqlIndexSuggestion */
76
+ /** @typedef {import("./v1/sql.js").V1SqlCheck} V1SqlCheck */
77
+ /** @typedef {import("./v1/changes.js").V1ChangeStreamRequest} V1ChangeStreamRequest */
78
+ /** @typedef {import("./v1/changes.js").V1DatabaseChange} V1DatabaseChange */
79
+ /** @typedef {import("./v1/changes.js").V1DatabaseChangeCursor} V1DatabaseChangeCursor */
80
+ /** @typedef {import("./v1/catalog.js").V1ManagedDatabase} V1ManagedDatabase */
81
+ /** @typedef {import("./v1/catalog.js").V1DatabaseCreateRequest} V1DatabaseCreateRequest */
82
+ /** @typedef {import("./v1/catalog.js").V1DatabaseUpdateRequest} V1DatabaseUpdateRequest */
83
+ /** @typedef {import("./v1/catalog.js").V1DatabaseBranchRequest} V1DatabaseBranchRequest */
84
+ /** @typedef {import("./v1/catalog.js").V1DatabaseListRequest} V1DatabaseListRequest */
85
+ /** @typedef {import("./v1/catalog.js").V1DatabasePage} V1DatabasePage */
86
+ /** @typedef {import("./v1/catalog.js").V1CollectionSchema} V1CollectionSchema */
87
+
88
+ export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
89
+ /** @typedef {import("./v1/database-schema.js").V1InferredSchema} V1InferredSchema */
90
+ /** @typedef {import("./v1/database-schema.js").V1DatabaseSchema} V1DatabaseSchema */
91
+ /** @typedef {import("./v1/database-schema.js").V1UniqueSchema} V1UniqueSchema */
92
+ /** @typedef {import("./v1/database-schema.js").V1UniqueSchemaRequest} V1UniqueSchemaRequest */
93
+ /** @typedef {import("./v1/database-schema.js").V1QueryShapes} V1QueryShapes */
94
+
95
+ export { V1Indexes } from "./v1/indexes.js";
96
+ /** @typedef {import("./v1/indexes.js").V1Index} V1Index */
97
+ /** @typedef {import("./v1/indexes.js").V1IndexList} V1IndexList */
98
+ /** @typedef {import("./v1/indexes.js").V1AutoIndexPolicy} V1AutoIndexPolicy */
99
+ /** @typedef {import("./v1/indexes.js").V1IndexRecommendations} V1IndexRecommendations */
100
+ /** @typedef {import("./v1/indexes.js").V1AppliedIndexRecommendation} V1AppliedIndexRecommendation */
101
+ /** @typedef {import("./v1/indexes.js").V1CreateIndexRequest} V1CreateIndexRequest */
102
+
103
+ export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
104
+ /** @typedef {import("./v1/usage.js").V1DatabaseUsage} V1DatabaseUsage */
105
+ /** @typedef {import("./v1/usage.js").V1ProjectUsage} V1ProjectUsage */
106
+ /** @typedef {import("./v1/usage.js").V1Analytics} V1Analytics */
107
+ /** @typedef {import("./v1/usage.js").V1Replication} V1Replication */
108
+ /** @typedef {import("./v1/usage.js").V1UsageRequest} V1UsageRequest */
109
+ /** @typedef {import("./v1/usage.js").V1AnalyticsRequest} V1AnalyticsRequest */
110
+
111
+ export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
112
+ /** @typedef {import("./v1/backups.js").V1Backup} V1Backup */
113
+ /** @typedef {import("./v1/backups.js").V1BackupPage} V1BackupPage */
114
+ /** @typedef {import("./v1/backups.js").V1BackupPolicy} V1BackupPolicy */
115
+ /** @typedef {import("./v1/backups.js").V1RestoreWindow} V1RestoreWindow */
116
+ /** @typedef {import("./v1/backups.js").V1PointInTimePlan} V1PointInTimePlan */
117
+ /** @typedef {import("./v1/backups.js").V1RestoredDatabase} V1RestoredDatabase */
118
+ /** @typedef {import("./v1/backups.js").V1BackupDestination} V1BackupDestination */
119
+ /** @typedef {import("./v1/backups.js").V1BackupPolicyRequest} V1BackupPolicyRequest */
120
+ /** @typedef {import("./v1/backups.js").V1PointInTimeRequest} V1PointInTimeRequest */
121
+ /** @typedef {import("./v1/backups.js").V1PointInTimeRestoreRequest} V1PointInTimeRestoreRequest */
122
+ /** @typedef {import("./v1/backups.js").V1BackupDestinationRequest} V1BackupDestinationRequest */
123
+
124
+ export { V1Imports } from "./v1/imports.js";
125
+ /** @typedef {import("./v1/imports.js").V1ImportJob} V1ImportJob */
126
+ /** @typedef {import("./v1/imports.js").V1ImportOptions} V1ImportOptions */
127
+ /** @typedef {import("./v1/imports.js").V1CreateImportRequest} V1CreateImportRequest */
128
+
129
+ export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.js";
130
+ /** @typedef {import("./v1/system.js").TerraBaseSystemClientConfig} TerraBaseSystemClientConfig */
131
+ /** @typedef {import("./v1/system.js").V1ProbeStatus} V1ProbeStatus */
132
+ /** @typedef {import("./v1/system.js").V1ReplicationHealth} V1ReplicationHealth */
133
+ /** @typedef {import("./v1/system.js").V1PlatformDatabases} V1PlatformDatabases */
134
+ /** @typedef {import("./v1/shape-manifest.js").V1ShapeManifest} V1ShapeManifest */
135
+ /** @typedef {import("./v1/shape-manifest.js").V1ShapeManifestRequest} V1ShapeManifestRequest */
136
+ /** @typedef {import("./v1/shape-reader.js").V1ShapeKeyRequest} V1ShapeKeyRequest */
137
+ /** @typedef {import("./v1/shape-reader.js").V1ShapeKey} V1ShapeKey */
@@ -14,7 +14,6 @@ export const localSqlitePresenceIndicators = /** @type {const} */ ([
14
14
  "NETLIFY",
15
15
  "CF_PAGES",
16
16
  "RENDER",
17
- "FLY_APP_NAME",
18
17
  "K_SERVICE",
19
18
  "CLOUD_RUN_JOB",
20
19
  "KUBERNETES_SERVICE_HOST",
package/src/ts-auth.js CHANGED
@@ -15,7 +15,6 @@ import { requireDeadLetterId } from "./ts-auth-ids.js";
15
15
  * @typedef {Omit<TsAuthRuntimeConfig, "apiBaseUrl"> & {
16
16
  * readonly apiBaseUrl: string; // The ts-auth deployment's public origin; there is no default.
17
17
  * readonly instanceSlug: string; // The instance the `sk_` key belongs to; it prefixes every route.
18
- * readonly useReverseProxy?: boolean; // Route public auth calls through the reverse proxy instead of regional discovery.
19
18
  * }} TsAuthClientConfig
20
19
  */
21
20
 
@@ -2360,7 +2359,6 @@ export class TsAuthOidcClient {
2360
2359
  routeName: "tsauth.oidc.resourceIntrospect",
2361
2360
  formBody: form,
2362
2361
  resourceIntrospectionBasic: basic,
2363
- issuer: expectedIssuer,
2364
2362
  }),
2365
2363
  /** @returns {value is TsAuthOidcResourceIntrospection} */
2366
2364
  (value) => {
@@ -2520,9 +2518,7 @@ export class TsAuthClient {
2520
2518
  );
2521
2519
  }
2522
2520
  const slug = validateInstanceSlug(config?.instanceSlug);
2523
- this.#http = new TsAuthHttp(config, {
2524
- authReverseProxy: config.useReverseProxy === true,
2525
- });
2521
+ this.#http = new TsAuthHttp(config);
2526
2522
  requireBareOrigin(this.#http.config.apiBaseUrl);
2527
2523
  this.users = new TsAuthUsersClient(this.#http, slug);
2528
2524
  this.sessions = new TsAuthSessionsClient(this.#http, slug);
@@ -0,0 +1,111 @@
1
+ import { requireId } from "../ids.js";
2
+ import { requireName } from "./ids.js";
3
+ import { flag, list, nullable, numeric, optional, record, text } from "./management-decode.js";
4
+ import { members, mutationOptions, pageQuery } from "./management-validation.js";
5
+ import { malformed, object } from "./decode.js";
6
+ import { stringifyExactJson } from "./json.js";
7
+ /** @import { V1Transport } from "./transport.js" */
8
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
9
+ const backup = record({ id: requireId, databaseId: requireId, status: text, trigger: text, sizeBytes: numeric, createdAt: text });
10
+ const backupPage = record({ items: list(backup), next: nullable(text) });
11
+ const policy = record({ fullBackupCron: nullable(text), retentionDays: numeric, disabled: flag, updatedAt: nullable(text) });
12
+ const window = record({ earliest: text, latest: text });
13
+ const plan = record({ window, pointInTime: text, snapshotAt: text, latestSegmentAt: nullable(text), segmentCount: numeric, historyCheck: text });
14
+ const restored = record({ id: requireId, projectId: requireId, name: text, branchedFrom: nullable(requireId), createdAt: text,
15
+ homeRegion: optional(nullable(text)), replicaRegions: optional(nullable(list(text))), placementVersion: optional(numeric), replicas: list(text) });
16
+ const destination = record({ endpoint: text, region: text, bucket: text, mode: text, updatedAt: text });
17
+ /** @typedef {ReturnType<typeof backup>} V1Backup */
18
+ /** @typedef {ReturnType<typeof backupPage>} V1BackupPage */
19
+ /** @typedef {ReturnType<typeof policy>} V1BackupPolicy */
20
+ /** @typedef {ReturnType<typeof window>} V1RestoreWindow */
21
+ /** @typedef {ReturnType<typeof plan>} V1PointInTimePlan */
22
+ /** @typedef {ReturnType<typeof restored>} V1RestoredDatabase */
23
+ /** @typedef {ReturnType<typeof destination>} V1BackupDestination */
24
+ /** @typedef {{ readonly fullBackupCron?: string | null; readonly retentionDays?: number | null; readonly disabled?: boolean | null }} V1BackupPolicyRequest */
25
+ /** @typedef {{ readonly pointInTime: string; readonly destination?: "platform" | "customer" | null }} V1PointInTimeRequest */
26
+ /** @typedef {V1PointInTimeRequest & { readonly name?: string | null }} V1PointInTimeRestoreRequest */
27
+ /** @typedef {{ readonly endpoint: string; readonly region: string; readonly bucket: string; readonly accessKeyId: string; readonly secretKey: string; readonly mode: "both" | "customer" }} V1BackupDestinationRequest */
28
+ /** @param {unknown} value @returns {"platform" | "customer"} */
29
+ function source(value) {
30
+ if (value !== "platform" && value !== "customer") throw new TypeError("destination must be platform or customer.");
31
+ return value;
32
+ }
33
+ /** @param {V1PointInTimeRequest} request */
34
+ function pointInTime(request) {
35
+ if (typeof request.pointInTime !== "string" || !Number.isFinite(Date.parse(request.pointInTime))) throw new TypeError("pointInTime must be an RFC 3339 instant.");
36
+ if (request.destination !== undefined && request.destination !== null) source(request.destination);
37
+ }
38
+
39
+ /** Backups and explicit point-in-time restore sources for one database. */
40
+ export class V1Backups {
41
+ /** @type {V1Transport} */
42
+ #transport;
43
+ /** @type {{database: string}} */
44
+ #params;
45
+ /** @param {V1Transport} transport @param {string} database */
46
+ constructor(transport, database) { this.#transport = transport; this.#params = { database: requireName("database", database) }; Object.freeze(this); }
47
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1Backup>} */
48
+ async create(options = {}) {
49
+ mutationOptions(options);
50
+ return backup((await this.#transport.call("createBackup", this.#params, { signal: options.signal })).value);
51
+ }
52
+ /** @param {import("./catalog.js").V1DatabaseListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1BackupPage>} */
53
+ async list(request = {}, options = {}) { return backupPage((await this.#transport.call("listBackups", this.#params, { query: pageQuery(request), signal: options.signal })).value); }
54
+ /** @param {string} id @param {{readonly name?: string | null}} [request] @param {V1RequestOptions} [options] @returns {Promise<V1RestoredDatabase>} */
55
+ async restore(id, request = {}, options = {}) {
56
+ members(request, ["name"]);
57
+ if (request.name !== undefined && request.name !== null) requireName("database", request.name);
58
+ mutationOptions(options);
59
+ return restored((await this.#transport.call("restoreBackup", { ...this.#params, backup: requireId(id) }, {
60
+ body: stringifyExactJson(request, "restore"), signal: options.signal,
61
+ })).value);
62
+ }
63
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1BackupPolicy>} */
64
+ async getPolicy(options = {}) { return policy((await this.#transport.call("getBackupPolicy", this.#params, { signal: options.signal })).value); }
65
+ /** @param {V1BackupPolicyRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1BackupPolicy>} */
66
+ async setPolicy(request, options = {}) {
67
+ members(request, ["fullBackupCron", "retentionDays", "disabled"]);
68
+ mutationOptions(options);
69
+ return policy((await this.#transport.call("setBackupPolicy", this.#params, { body: stringifyExactJson(request, "backup policy"), signal: options.signal })).value);
70
+ }
71
+ /** @param {{readonly destination?: "platform" | "customer"}} [request] @param {V1RequestOptions} [options] @returns {Promise<V1RestoreWindow>} */
72
+ async window(request = {}, options = {}) {
73
+ members(request, ["destination"]);
74
+ const query = new URLSearchParams();
75
+ if (request.destination !== undefined) query.set("destination", source(request.destination));
76
+ return window((await this.#transport.call("getRestoreWindow", this.#params, { query, signal: options.signal })).value);
77
+ }
78
+ /** @param {V1PointInTimeRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1PointInTimePlan>} */
79
+ async plan(request, options = {}) {
80
+ members(request, ["pointInTime", "destination"]);
81
+ pointInTime(request);
82
+ return plan((await this.#transport.call("planPointInTime", this.#params, { body: stringifyExactJson(request, "restore plan"), signal: options.signal })).value);
83
+ }
84
+ /** @param {V1PointInTimeRestoreRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1RestoredDatabase>} */
85
+ async restorePointInTime(request, options = {}) {
86
+ members(request, ["pointInTime", "destination", "name"]);
87
+ pointInTime(request);
88
+ if (request.name !== undefined && request.name !== null) requireName("database", request.name);
89
+ mutationOptions(options);
90
+ return restored((await this.#transport.call("restorePointInTime", this.#params, { body: stringifyExactJson(request, "point-in-time restore"), signal: options.signal })).value);
91
+ }
92
+ }
93
+ /** Write-only destination credentials are sent only to the configured API, never retained in the client. */
94
+ export class V1BackupDestinationClient {
95
+ /** @type {V1Transport} */
96
+ #transport;
97
+ /** @param {V1Transport} transport */
98
+ constructor(transport) { this.#transport = transport; Object.freeze(this); }
99
+ /** @param {V1RequestOptions} [options] @returns {Promise<V1BackupDestination>} */
100
+ async get(options = {}) { return destination((await this.#transport.call("getBackupDestination", {}, { signal: options.signal })).value); }
101
+ /** @param {V1BackupDestinationRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1BackupDestination>} */
102
+ async set(request, options = {}) {
103
+ members(request, ["endpoint", "region", "bucket", "accessKeyId", "secretKey", "mode"]);
104
+ mutationOptions(options);
105
+ const raw = object((await this.#transport.call("setBackupDestination", {}, { body: stringifyExactJson(request, "backup destination"), signal: options.signal })).value, "backup destination");
106
+ if (raw.accessKeyId !== undefined || raw.secretKey !== undefined) malformed("write-only destination credentials");
107
+ return destination(raw);
108
+ }
109
+ /** @param {V1RequestOptions} [options] @returns {Promise<void>} */
110
+ async delete(options = {}) { mutationOptions(options); await this.#transport.call("deleteBackupDestination", {}, { signal: options.signal }); }
111
+ }
@@ -0,0 +1,210 @@
1
+ import { requireId } from "../ids.js";
2
+ import { array, malformed, object, string } from "./decode.js";
3
+ import { requireName } from "./ids.js";
4
+ import { isJsonObject, stringifyExactJson } from "./json.js";
5
+ /**
6
+ * @import { JsonInputObject, JsonObject, JsonValue } from "./json.js"
7
+ * @import { V1Transport } from "./transport.js"
8
+ * @import { V1RequestOptions } from "./terrabase-v1.js"
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} V1ManagedDatabase
13
+ * @property {string} id Canonical database ULID.
14
+ * @property {string} projectId Canonical owning-project ULID.
15
+ * @property {string} name Database name used in data routes.
16
+ * @property {string | null} branchedFrom Source database ULID, or null.
17
+ * @property {string} createdAt ISO date-time returned by the producer.
18
+ * @property {string} region Serving region.
19
+ * @property {string} homeRegion Management region.
20
+ * @property {readonly string[]} replicaRegions Other serving regions; empty when allRegions is true.
21
+ * @property {boolean} allRegions True when a full copy is served in every serving region, including later ones.
22
+ */
23
+ /** @typedef {{ readonly name: string; readonly homeRegion?: string | null | undefined; readonly replicaRegions?: readonly string[] | null | undefined; readonly allRegions?: boolean | undefined }} V1DatabaseCreateRequest */
24
+ /** @typedef {{ readonly replicaRegions?: readonly string[] | undefined; readonly allRegions?: true | undefined; readonly homeRegion?: string | null | undefined }} V1DatabaseUpdateRequest */
25
+ /** @typedef {{ readonly name: string }} V1DatabaseBranchRequest */
26
+ /** @typedef {{ readonly after?: string | undefined; readonly limit?: number | undefined }} V1DatabaseListRequest */
27
+ /** @typedef {{ readonly items: readonly V1ManagedDatabase[]; readonly next: string | null }} V1DatabasePage */
28
+ /** @typedef {{ readonly schema: JsonObject | boolean | null; readonly dialect?: string | null | undefined }} V1CollectionSchema */
29
+
30
+ /** @param {unknown} value @param {readonly string[]} allowed @returns {void} */
31
+ function members(value, allowed) {
32
+ if (typeof value !== "object" || value === null || Array.isArray(value) ||
33
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
34
+ throw new TypeError("A catalog request must be a plain object.");
35
+ for (const key of Object.keys(value))
36
+ if (!allowed.includes(key)) throw new TypeError(`Unknown catalog request member ${key}.`);
37
+ }
38
+
39
+ /** @param {object} options @returns {void} */
40
+ function mutationOptions(options) {
41
+ if ("idempotencyKey" in options)
42
+ throw new TypeError("The current producer does not support Idempotency-Key; mutations are never automatically retried.");
43
+ }
44
+
45
+ /** @param {unknown} value @returns {string} */
46
+ function region(value) {
47
+ if (typeof value !== "string" || !/^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(value))
48
+ throw new TypeError("A region is 1-63 lowercase letters, digits or hyphens, starts with a letter and cannot end with a hyphen.");
49
+ return value;
50
+ }
51
+
52
+ /** @param {unknown} value @param {string | null | undefined} home @returns {readonly string[]} */
53
+ function replicas(value, home) {
54
+ if (!Array.isArray(value) || value.length > 8) throw new TypeError("replicaRegions is an array of at most 8 regions.");
55
+ const result = value.map(region);
56
+ if (new Set(result).size !== result.length) throw new TypeError("replicaRegions must not repeat a region.");
57
+ if (home !== undefined && home !== null && result.includes(home))
58
+ throw new TypeError("replicaRegions must not include homeRegion.");
59
+ return result;
60
+ }
61
+
62
+ /** @param {JsonValue | undefined} value @returns {V1ManagedDatabase} */
63
+ function database(value) {
64
+ const raw = object(value, "managed database");
65
+ const createdAt = string(raw.createdAt, "database createdAt");
66
+ if (!/^\d{4}-\d{2}-\d{2}T/u.test(createdAt) || !Number.isFinite(Date.parse(createdAt))) malformed("database createdAt");
67
+ if (typeof raw.allRegions !== "boolean") malformed("database allRegions");
68
+ return Object.freeze({
69
+ id: requireId(raw.id, "Database ID"), projectId: requireId(raw.projectId, "Project ID"),
70
+ name: requireName("database", raw.name),
71
+ branchedFrom: raw.branchedFrom === null ? null : requireId(raw.branchedFrom, "Source database ID"),
72
+ createdAt, region: region(raw.region), homeRegion: region(raw.homeRegion),
73
+ replicaRegions: Object.freeze(array(raw.replicaRegions, "database replicaRegions").map(region)),
74
+ allRegions: raw.allRegions,
75
+ });
76
+ }
77
+
78
+ /** Public database catalog operations, bound to the transport's authorized project. */
79
+ export class V1Databases {
80
+ /** @type {V1Transport} */
81
+ #transport;
82
+ /** @param {V1Transport} transport */
83
+ constructor(transport) { this.#transport = transport; Object.freeze(this); }
84
+
85
+ /** @param {V1DatabaseCreateRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1ManagedDatabase>} */
86
+ async create(request, options = {}) {
87
+ members(request, ["name", "homeRegion", "replicaRegions", "allRegions"]);
88
+ mutationOptions(options);
89
+ const name = requireName("database", request.name);
90
+ if (request.homeRegion !== undefined && request.homeRegion !== null) region(request.homeRegion);
91
+ if (request.replicaRegions !== undefined && request.replicaRegions !== null) replicas(request.replicaRegions, request.homeRegion);
92
+ if (request.allRegions !== undefined && typeof request.allRegions !== "boolean") throw new TypeError("allRegions must be a boolean.");
93
+ if (request.allRegions === true && request.replicaRegions !== undefined && request.replicaRegions !== null)
94
+ throw new TypeError("allRegions cannot be combined with replicaRegions.");
95
+ const response = await this.#transport.call("createDatabase", { database: name }, {
96
+ body: stringifyExactJson({ name,
97
+ ...(request.homeRegion === undefined ? {} : { homeRegion: request.homeRegion }),
98
+ ...(request.replicaRegions === undefined ? {} : { replicaRegions: request.replicaRegions }),
99
+ ...(request.allRegions === undefined ? {} : { allRegions: request.allRegions }),
100
+ }, "database create"), signal: options.signal,
101
+ });
102
+ return database(response.value);
103
+ }
104
+
105
+ /** @param {V1DatabaseListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<V1DatabasePage>} */
106
+ async list(request = {}, options = {}) {
107
+ members(request, ["after", "limit"]);
108
+ const query = new URLSearchParams();
109
+ if (request.after !== undefined) {
110
+ if (typeof request.after !== "string") throw new TypeError("Database after must be a string.");
111
+ query.set("after", request.after);
112
+ }
113
+ if (request.limit !== undefined) {
114
+ if (!Number.isInteger(request.limit) || request.limit < 1 || request.limit > 100) throw new TypeError("Database list limit is 1-100.");
115
+ query.set("limit", String(request.limit));
116
+ }
117
+ const response = await this.#transport.call("listDatabases", {}, { query, signal: options.signal });
118
+ const raw = object(response.value, "database page");
119
+ if (raw.next !== null && (typeof raw.next !== "string" || raw.next === "")) malformed("database next");
120
+ return Object.freeze({ items: Object.freeze(array(raw.items, "databases").map(database)), next: raw.next });
121
+ }
122
+
123
+ /** @param {string} name @param {V1RequestOptions} [options] @returns {Promise<V1ManagedDatabase>} */
124
+ async get(name, options = {}) {
125
+ const response = await this.#transport.call("getDatabase", { database: requireName("database", name) }, { signal: options.signal });
126
+ return database(response.value);
127
+ }
128
+
129
+ /** @param {string} name @param {V1DatabaseUpdateRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1ManagedDatabase>} */
130
+ async update(name, request, options = {}) {
131
+ members(request, ["homeRegion", "replicaRegions", "allRegions"]);
132
+ mutationOptions(options);
133
+ if (request.homeRegion !== undefined && request.homeRegion !== null) region(request.homeRegion);
134
+ if (request.allRegions === true) {
135
+ if (request.replicaRegions !== undefined) throw new TypeError("allRegions cannot be combined with replicaRegions.");
136
+ } else {
137
+ if (request.allRegions !== undefined) throw new TypeError("allRegions must be true; send replicaRegions to turn it off.");
138
+ replicas(request.replicaRegions, request.homeRegion);
139
+ }
140
+ const response = await this.#transport.call("updateDatabase", { database: requireName("database", name) }, {
141
+ body: stringifyExactJson({
142
+ ...(request.allRegions === true ? { allRegions: true } : { replicaRegions: request.replicaRegions }),
143
+ ...(request.homeRegion === undefined ? {} : { homeRegion: request.homeRegion }),
144
+ }, "database update"), signal: options.signal,
145
+ });
146
+ return database(response.value);
147
+ }
148
+
149
+ /** @param {string} name @param {V1RequestOptions} [options] @returns {Promise<void>} */
150
+ async delete(name, options = {}) {
151
+ mutationOptions(options);
152
+ await this.#transport.call("deleteDatabase", { database: requireName("database", name) }, { signal: options.signal });
153
+ }
154
+
155
+ /** @param {string} name @param {V1DatabaseBranchRequest} request @param {V1RequestOptions} [options] @returns {Promise<V1ManagedDatabase>} */
156
+ async branch(name, request, options = {}) {
157
+ members(request, ["name"]);
158
+ mutationOptions(options);
159
+ const response = await this.#transport.call("branchDatabase", { database: requireName("database", name) }, {
160
+ body: stringifyExactJson({ name: requireName("database", request.name) }, "database branch"), signal: options.signal,
161
+ });
162
+ return database(response.value);
163
+ }
164
+ }
165
+
166
+ /** @param {JsonValue | undefined} value @returns {V1CollectionSchema} */
167
+ function collectionSchema(value) {
168
+ const raw = object(value, "collection schema");
169
+ const schema = raw.schema;
170
+ if (schema !== null && typeof schema !== "boolean" && !isJsonObject(schema)) malformed("declared schema");
171
+ const dialect = raw.dialect;
172
+ if (dialect !== undefined && dialect !== null && typeof dialect !== "string") malformed("schema dialect");
173
+ return Object.freeze({ schema, ...(dialect === undefined ? {} : { dialect }) });
174
+ }
175
+
176
+ /** @param {boolean | JsonInputObject} schema @returns {string} */
177
+ function schemaText(schema) {
178
+ if (typeof schema !== "boolean" && !isJsonObject(schema)) throw new TypeError("A declared schema must be a JSON object or boolean.");
179
+ const body = stringifyExactJson(schema, "declared schema");
180
+ if (new TextEncoder().encode(body).byteLength > 64 * 1024) throw new TypeError("A declared schema is at most 64 KiB.");
181
+ let nodes = 0;
182
+ /** @param {unknown} value @param {number} depth */
183
+ const visit = (value, depth) => {
184
+ if (++nodes > 4093) throw new TypeError("A declared schema holds at most 4093 values.");
185
+ if (!Array.isArray(value) && !isJsonObject(value)) return;
186
+ if (depth >= 15) throw new TypeError("A declared schema nests at most 15 levels.");
187
+ for (const child of Object.values(value)) visit(child, depth + 1);
188
+ };
189
+ visit(schema, 0);
190
+ return body;
191
+ }
192
+
193
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionSchema>} */
194
+ export async function getCollectionSchema(transport, params, options = {}) {
195
+ return collectionSchema((await transport.call("getCollectionSchema", params, { signal: options.signal })).value);
196
+ }
197
+
198
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {boolean | JsonInputObject} schema @param {V1RequestOptions} [options] @returns {Promise<V1CollectionSchema>} */
199
+ export async function putCollectionSchema(transport, params, schema, options = {}) {
200
+ mutationOptions(options);
201
+ return collectionSchema((await transport.call("putCollectionSchema", params, {
202
+ body: schemaText(schema), signal: options.signal,
203
+ })).value);
204
+ }
205
+
206
+ /** @param {V1Transport} transport @param {Readonly<Record<string, string>>} params @param {V1RequestOptions} [options] @returns {Promise<V1CollectionSchema>} */
207
+ export async function deleteCollectionSchema(transport, params, options = {}) {
208
+ mutationOptions(options);
209
+ return collectionSchema((await transport.call("deleteCollectionSchema", params, { signal: options.signal })).value);
210
+ }