@terrascale/sdk 0.0.5 → 0.0.6

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.6 — relational SQL
4
+
5
+ `db.execute(sql, args)`, `db.query(sql, args)` and `db.batch(statements)` call
6
+ TerraBase's `POST /v1/projects/{project}/databases/{database}/sql/execute`.
7
+ Each request is one transaction. Results are typed
8
+ `{ columns, rows, rowsAffected, lastInsertRowid }`; a refused statement throws
9
+ `SqlError` (a `TerraBaseV1Error`) with `statement` and `sqliteCode`. The
10
+ read-only `db.sql` route is unchanged.
11
+
3
12
  ## Unreleased — collection TTL and vector near sort
4
13
 
5
14
  `collection.putTtl(path)`, `getTtl()` and `deleteTtl()` call
package/README.md CHANGED
@@ -110,7 +110,8 @@ OpenAPI and immutable release qualification remain open gates.
110
110
  | Collections and declared JSON Schema | `db.collections.create/get/list/drop`; `collection.getSchema/putSchema/deleteSchema` |
111
111
  | Collection TTL | `collection.putTtl/getTtl/deleteTtl` |
112
112
  | Databases | `client.databases.create/list/get/update/delete/branch` |
113
- | SQL | `db.sql/checkSql/explainSql` |
113
+ | Relational SQL | `db.execute/query/batch` |
114
+ | Read-only SQL (older) | `db.sql/checkSql/explainSql` |
114
115
  | Changes | `db.changes` (SSE async iterator) |
115
116
  | Export | `db.export` (NDJSON async iterator) |
116
117
 
@@ -161,6 +162,37 @@ or replay mutations after an uncertain result; this release does not promise
161
162
  mutation deduplication. Validation, authentication, authorization and
162
163
  precondition failures are not automatically retried.
163
164
 
165
+ Relational SQL runs statements on tables created with `CREATE TABLE` (rows
166
+ are documents). `execute` runs one statement, `query` returns its rows as
167
+ objects keyed by column name, and `batch` sends 1 to 256 statements as one
168
+ transaction: all commit or none do. `BEGIN`, `COMMIT` and `ROLLBACK` are
169
+ refused. Arguments are positional (`?`) or named (`:name`, `@name`, `$name`,
170
+ passed without the prefix). Each is `null`, a number, an int64 `bigint`, a
171
+ boolean (stored as 1 or 0) or a string. Reads need `documents:read`; writes and
172
+ DDL also need `documents:write`.
173
+
174
+ ```js
175
+ await db.batch([
176
+ "CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
177
+ { sql: "INSERT INTO people (id, name) VALUES (?, ?)", args: [1, "Ada"] },
178
+ { sql: "INSERT INTO people (id, name) VALUES (:id, :name)", named: { id: 2, name: "Grace" } },
179
+ ]);
180
+ const people = await db.query("SELECT id, name FROM people WHERE id > ?", [0]);
181
+ const { columns, rows, rowsAffected, lastInsertRowid } = await db.execute("DELETE FROM people WHERE id = :id", { id: 2 });
182
+ ```
183
+
184
+ A refused statement throws `SqlError`, a `TerraBaseV1Error` that adds
185
+ `statement` (zero-based index) and `sqliteCode` (such as
186
+ `SQLITE_CONSTRAINT_PRIMARYKEY`). Its `code` is `sql_error` (400),
187
+ `forbidden_scope` (403), `sql_conflict` (409) or `sql_too_large` (413).
188
+ Authentication, quota and availability errors keep their usual error classes.
189
+
190
+ TerraBase sends integers as JSON numbers. A plain JavaScript number cannot hold
191
+ an integer above 2^53 exactly, so the SDK decodes such a value (a row value or
192
+ `lastInsertRowid`) as an `ExactDecimal` that keeps every digit. Convert it with
193
+ `BigInt(String(value))`. Pass large integer arguments as `bigint`.
194
+
195
+ The older `db.sql` route below is read-only.
164
196
  SQL is the producer's bounded read-only subset: `{ matched, truncated, rows }`,
165
197
  with row `{ id, value }` and no pagination cursor. `checkSql` returns `ok: false`
166
198
  for invalid SQL; `explainSql` returns an HTTP error for invalid SQL. All three
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrascale/sdk",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "JavaScript SDK, typed with JSDoc, for project-scoped C# TerraBase v1 and hosted TerraScale identity.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -71,6 +71,7 @@
71
71
  {"method":"PUT","path":"/v1/projects/{project}/databases/{database}/schema/{collection}","operation":"declareCollectionSchema","scopes":["databases:write"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Schema/PutCollectionSchemaEndpoint.cs","request":"UniqueSchemaRequest"},
72
72
  {"method":"POST","path":"/v1/projects/{project}/databases/{database}/sql","operation":"runSql","scopes":["databases:read"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Queries/RunSqlEndpoint.cs","request":"SqlRequest"},
73
73
  {"method":"POST","path":"/v1/projects/{project}/databases/{database}/sql/check","operation":"checkSql","scopes":["databases:read"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Queries/CheckSqlEndpoint.cs","request":"SqlRequest"},
74
+ {"method":"POST","path":"/v1/projects/{project}/databases/{database}/sql/execute","operation":"executeSql","scopes":["documents:read"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Sql/SqlExecuteEndpoint.cs","request":"SqlExecuteRequest","producerChange":"TerraScale/terrascale#420"},
74
75
  {"method":"POST","path":"/v1/projects/{project}/databases/{database}/sql/explain","operation":"explainSql","scopes":["databases:read"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Queries/ExplainSqlEndpoint.cs","request":"SqlRequest"},
75
76
  {"method":"POST","path":"/v1/projects/{project}/databases/{database}/transactions","operation":"runTransaction","scopes":["documents:write"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Documents/TransactionEndpoint.cs","request":"TransactionRequest"},
76
77
  {"method":"GET","path":"/v1/projects/{project}/databases/{database}/usage","operation":"getDatabaseUsage","scopes":["databases:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Usage/GetDatabaseUsageEndpoint.cs","request":"none"},
@@ -80,6 +81,10 @@
80
81
  {"method":"DELETE","path":"/v1/projects/{project}/saved-queries/{query}","operation":"deleteSavedQuery","scopes":["databases:write"],"requestMediaTypes":[],"statuses":[204],"responseMediaType":"","source":"src/TerraBase/Features/Management/Queries/DeleteSavedQueryEndpoint.cs","request":"none"},
81
82
  {"method":"GET","path":"/v1/projects/{project}/saved-queries/{query}","operation":"getSavedQuery","scopes":["databases:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Queries/GetSavedQueryEndpoint.cs","request":"none"},
82
83
  {"method":"PUT","path":"/v1/projects/{project}/saved-queries/{query}","operation":"updateSavedQuery","scopes":["databases:write"],"requestMediaTypes":["application/json"],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Queries/UpdateSavedQueryEndpoint.cs","request":"SavedQueryRequest"},
83
- {"method":"GET","path":"/v1/projects/{project}/usage","operation":"getProjectUsage","scopes":["databases:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Usage/GetProjectUsageEndpoint.cs","request":"none"}
84
+ {"method":"GET","path":"/v1/projects/{project}/usage","operation":"getProjectUsage","scopes":["databases:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","source":"src/TerraBase/Features/Management/Usage/GetProjectUsageEndpoint.cs","request":"none"},
85
+ {"method":"POST","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/webhooks","operation":"createChangeWebhook","scopes":["documents:read","documents:write"],"requestMediaTypes":["application/json"],"statuses":[201],"responseMediaType":"application/json","contract":"contracts/change-webhooks.md","request":"CreateWebhookRequest"},
86
+ {"method":"GET","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/webhooks","operation":"listChangeWebhooks","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","contract":"contracts/change-webhooks.md","request":"none"},
87
+ {"method":"DELETE","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/webhooks/{webhook}","operation":"deleteChangeWebhook","scopes":["documents:read","documents:write"],"requestMediaTypes":[],"statuses":[204],"responseMediaType":"","contract":"contracts/change-webhooks.md","request":"none"},
88
+ {"method":"GET","path":"/v1/projects/{project}/databases/{database}/collections/{collection}/webhooks/{webhook}/deliveries","operation":"listChangeWebhookDeliveries","scopes":["documents:read"],"requestMediaTypes":[],"statuses":[200],"responseMediaType":"application/json","contract":"contracts/change-webhooks.md","request":"none"}
84
89
  ]
85
90
  }
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  V1InvalidKeyError,
16
16
  V1ForbiddenScopeError,
17
17
  V1NotFoundError,
18
+ SqlError,
18
19
  V1ErrorCode,
19
20
  isTerraBaseV1Error,
20
21
  } from "./v1/errors.js";
@@ -76,6 +77,12 @@ export { V1SavedQueries } from "./v1/saved-queries.js";
76
77
  /** @typedef {import("./v1/sql.js").V1SqlResult} V1SqlResult */
77
78
  /** @typedef {import("./v1/sql.js").V1SqlIndexSuggestion} V1SqlIndexSuggestion */
78
79
  /** @typedef {import("./v1/sql.js").V1SqlCheck} V1SqlCheck */
80
+ /** @typedef {import("./v1/sql-execute.js").V1SqlArgument} V1SqlArgument */
81
+ /** @typedef {import("./v1/sql-execute.js").V1SqlArguments} V1SqlArguments */
82
+ /** @typedef {import("./v1/sql-execute.js").V1SqlStatement} V1SqlStatement */
83
+ /** @typedef {import("./v1/sql-execute.js").V1SqlValue} V1SqlValue */
84
+ /** @typedef {import("./v1/sql-execute.js").V1SqlColumn} V1SqlColumn */
85
+ /** @typedef {import("./v1/sql-execute.js").V1SqlExecuteResult} V1SqlExecuteResult */
79
86
  /** @typedef {import("./v1/changes.js").V1ChangeStreamRequest} V1ChangeStreamRequest */
80
87
  /** @typedef {import("./v1/changes.js").V1DatabaseChange} V1DatabaseChange */
81
88
  /** @typedef {import("./v1/changes.js").V1DatabaseChangeCursor} V1DatabaseChangeCursor */
@@ -114,6 +121,11 @@ export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
114
121
  /** @typedef {import("./v1/usage.js").V1AnalyticsRequest} V1AnalyticsRequest */
115
122
 
116
123
  export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
124
+ export { V1ChangeWebhooks } from "./v1/change-webhooks.js";
125
+ /** @typedef {import("./v1/change-webhooks.js").V1ChangeWebhook} V1ChangeWebhook */
126
+ /** @typedef {import("./v1/change-webhooks.js").V1CreatedChangeWebhook} V1CreatedChangeWebhook */
127
+ /** @typedef {import("./v1/change-webhooks.js").V1ChangeWebhookDelivery} V1ChangeWebhookDelivery */
128
+ /** @typedef {import("./v1/change-webhooks.js").V1ChangeWebhookListRequest} V1ChangeWebhookListRequest */
117
129
  /** @typedef {import("./v1/backups.js").V1Backup} V1Backup */
118
130
  /** @typedef {import("./v1/backups.js").V1BackupPage} V1BackupPage */
119
131
  /** @typedef {import("./v1/backups.js").V1BackupPolicy} V1BackupPolicy */
@@ -0,0 +1,78 @@
1
+ import { requireId } from "../ids.js";
2
+ import { requireName } from "./ids.js";
3
+ import { object, string } from "./decode.js";
4
+ import { list, nullable, numeric, record, text } from "./management-decode.js";
5
+ import { members, mutationOptions, pageQuery } from "./management-validation.js";
6
+ import { stringifyExactJson } from "./json.js";
7
+ /** @import { V1Transport } from "./transport.js" */
8
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
9
+ const webhook = record({ id: requireId, url: text, createdAt: text, status: text });
10
+ const webhooks = list(webhook);
11
+ const deliveries = list(record({ id: requireId, createdAt: text, status: text, attempts: numeric,
12
+ lastAttemptAt: nullable(text), nextAttemptAt: nullable(text), httpStatus: nullable(numeric), errorCode: nullable(text) }));
13
+ /** @typedef {ReturnType<typeof webhook>} V1ChangeWebhook */
14
+ /** @typedef {V1ChangeWebhook & { readonly secret: string }} V1CreatedChangeWebhook */
15
+ /** @typedef {ReturnType<typeof deliveries>[number]} V1ChangeWebhookDelivery */
16
+ /** @typedef {{ readonly after?: string; readonly limit?: number }} V1ChangeWebhookListRequest */
17
+
18
+ /** @param {V1ChangeWebhookListRequest} request */
19
+ function query(request) {
20
+ if (request.after !== undefined) requireId(request.after, "after");
21
+ return pageQuery(request);
22
+ }
23
+
24
+ /** Collection change webhook registration and sanitized delivery history. Requests are never replayed. */
25
+ export class V1ChangeWebhooks {
26
+ /** @type {V1Transport} */
27
+ #transport;
28
+ /** @type {{database: string; collection: string}} */
29
+ #params;
30
+ /** @param {V1Transport} transport @param {string} database @param {string} collection */
31
+ constructor(transport, database, collection) {
32
+ this.#transport = transport;
33
+ this.#params = { database: requireName("database", database), collection: requireName("collection", collection) };
34
+ Object.freeze(this);
35
+ }
36
+ /**
37
+ * Registers from the current feed head. Requires documents:read and documents:write.
38
+ * Store secret immediately; it is non-enumerable to keep it out of JSON and ordinary diagnostics.
39
+ * @param {{ readonly url: string }} request @param {V1RequestOptions} [options] @returns {Promise<V1CreatedChangeWebhook>}
40
+ */
41
+ async create(request, options = {}) {
42
+ members(request, ["url"]);
43
+ if (typeof request.url !== "string" || request.url.length > 2048) throw new TypeError("Webhook URL must be an HTTPS URL of at most 2048 characters.");
44
+ let url;
45
+ try { url = new URL(request.url); } catch { throw new TypeError("Webhook URL must be absolute HTTPS."); }
46
+ if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "" || url.hash !== "")
47
+ throw new TypeError("Webhook URL must be HTTPS on port 443, without credentials or a fragment.");
48
+ mutationOptions(options);
49
+ const raw = object((await this.#transport.call("createChangeWebhook", this.#params, {
50
+ body: stringifyExactJson(request, "change webhook"), signal: options.signal,
51
+ })).value, "created webhook");
52
+ const secret = string(raw.secret, "webhook secret");
53
+ return Object.freeze(Object.defineProperty({ ...webhook(raw), secret }, "secret", { enumerable: false }));
54
+ }
55
+ /**
56
+ * Requires documents:read. Ascending IDs; continue with the final ID until an empty array. No secrets.
57
+ * @param {V1ChangeWebhookListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<readonly V1ChangeWebhook[]>}
58
+ */
59
+ async list(request = {}, options = {}) {
60
+ return webhooks((await this.#transport.call("listChangeWebhooks", this.#params, { query: query(request), signal: options.signal })).value);
61
+ }
62
+ /**
63
+ * Requires documents:read and documents:write. An in-flight delivery may still complete. Unknown ID returns 404.
64
+ * @param {string} id @param {V1RequestOptions} [options] @returns {Promise<void>}
65
+ */
66
+ async delete(id, options = {}) {
67
+ mutationOptions(options);
68
+ await this.#transport.call("deleteChangeWebhook", { ...this.#params, webhook: requireId(id) }, { signal: options.signal });
69
+ }
70
+ /**
71
+ * Requires documents:read. Latest 100 retained records; paginate with the final ID until an empty array.
72
+ * @param {string} id @param {V1ChangeWebhookListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<readonly V1ChangeWebhookDelivery[]>}
73
+ */
74
+ async deliveries(id, request = {}, options = {}) {
75
+ return deliveries((await this.#transport.call("listChangeWebhookDeliveries", { ...this.#params, webhook: requireId(id) },
76
+ { query: query(request), signal: options.signal })).value);
77
+ }
78
+ }
package/src/v1/errors.js CHANGED
@@ -76,6 +76,7 @@ export const V1ErrorCode = Object.freeze({
76
76
  mustRefetch: "must_refetch", preconditionFailed: "precondition_failed", payloadTooLarge: "payload_too_large",
77
77
  unsupportedMediaType: "unsupported_media_type", uriTooLong: "uri_too_long", shapeBusy: "shape_busy",
78
78
  internalError: "internal_error", capacity: "capacity", notModified: "not_modified",
79
+ sqlError: "sql_error", sqlConflict: "sql_conflict", sqlTooLarge: "sql_too_large",
79
80
  });
80
81
 
81
82
  /** `412 precondition_failed`: an If-Match, If-None-Match, assert or assert_count failed. */
@@ -114,11 +115,36 @@ export class V1NotFoundError extends TerraBaseV1Error {
114
115
  }
115
116
  }
116
117
 
118
+ /**
119
+ * A refused relational SQL statement: `sql_error` (400), `forbidden_scope` (403, writes and DDL need
120
+ * documents:write), `sql_conflict` (409) or `sql_too_large` (413). The whole request rolled back.
121
+ */
122
+ export class SqlError extends TerraBaseV1Error {
123
+ /** @type {number} Zero-based index of the failing statement. */
124
+ statement;
125
+ /** @type {string | undefined} The SQLite error code, such as `SQLITE_CONSTRAINT_PRIMARYKEY`. */
126
+ sqliteCode;
127
+
128
+ /** @param {TerraBaseV1ErrorInit & { statement: number; sqliteCode?: string | undefined }} init */
129
+ constructor(init) {
130
+ super(init);
131
+ this.name = "SqlError";
132
+ this.statement = init.statement;
133
+ this.sqliteCode = init.sqliteCode;
134
+ }
135
+ }
136
+
117
137
  /**
118
138
  * @param {TerraBaseV1ErrorInit} init
119
139
  * @returns {TerraBaseV1Error}
120
140
  */
121
141
  function typedError(init) {
142
+ // Only the SQL execute route reports a statement index.
143
+ const statement = init.details?.statement;
144
+ if (typeof statement === "number" && Number.isInteger(statement) && statement >= 0) {
145
+ const sqliteCode = init.details?.sqlite_code;
146
+ return new SqlError({ ...init, statement, sqliteCode: typeof sqliteCode === "string" ? sqliteCode : undefined });
147
+ }
122
148
  switch (init.code) {
123
149
  case "precondition_failed": return new V1PreconditionFailedError(init);
124
150
  case "invalid_key": return new V1InvalidKeyError(init);
@@ -1,10 +1,17 @@
1
- /** @typedef {"getShapeManifest" | "exportDatabase" | "putCollectionTtl" | "getCollectionTtl" | "deleteCollectionTtl" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery"} ManagementOperation */
1
+ /** @typedef {"getShapeManifest" | "exportDatabase" | "executeSql" | "putCollectionTtl" | "getCollectionTtl" | "deleteCollectionTtl" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery" | "createChangeWebhook" | "listChangeWebhooks" | "deleteChangeWebhook" | "listChangeWebhookDeliveries"} ManagementOperation */
2
+ const webhooks = "/v1/projects/{project}/databases/{database}/collections/{collection}/webhooks";
2
3
  const saved = "/v1/projects/{project}/saved-queries";
3
4
  const ttl = "/v1/projects/{project}/databases/{database}/collections/{collection}/ttl";
4
5
  /** @type {readonly import("./routes.js").V1Route[]} */
5
6
  export const managementV1Routes = Object.freeze([
7
+ route("createChangeWebhook", "POST", webhooks, 201),
8
+ route("listChangeWebhooks", "GET", webhooks),
9
+ route("deleteChangeWebhook", "DELETE", `${webhooks}/{webhook}`, 204),
10
+ route("listChangeWebhookDeliveries", "GET", `${webhooks}/{webhook}/deliveries`),
6
11
  route("getShapeManifest", "GET", "/v1/projects/{project}/databases/{database}/collections/{collection}/shape/manifest"),
7
12
  route("exportDatabase", "GET", "/v1/projects/{project}/databases/{database}/export"),
13
+ // Reads need documents:read; writes and DDL also need documents:write, checked per statement.
14
+ route("executeSql", "POST", "/v1/projects/{project}/databases/{database}/sql/execute"),
8
15
  route("putCollectionTtl", "PUT", ttl),
9
16
  route("getCollectionTtl", "GET", ttl),
10
17
  route("deleteCollectionTtl", "DELETE", ttl),
@@ -61,12 +68,13 @@ export const managementV1Routes = Object.freeze([
61
68
  /** @param {ManagementOperation} operation @param {import("./routes.js").V1Route["method"]} method @param {string} pathTemplate @param {number} [status] @returns {import("./routes.js").V1Route} */
62
69
  function route(operation, method, pathTemplate, status = 200) {
63
70
  const write = method !== "GET" && operation !== "planPointInTime";
71
+ const webhook = pathTemplate.startsWith(webhooks);
64
72
  const noBody = ["createBackup", "applyIndexRecommendation", "cancelImport", "resumeImport"].includes(operation);
65
73
  return Object.freeze({ operation, method, pathTemplate,
66
- permission: operation === "getShapeManifest" || operation === "exportDatabase" || operation === "getCollectionTtl" ? "documents:read" : pathTemplate === ttl ? "documents:write" : pathTemplate.startsWith("/v1/platform/") ? "platform-admin" : !pathTemplate.startsWith("/v1/") ? "anonymous" : write ? "databases:write" : "databases:read",
67
- additionalPermissions: Object.freeze(["createImport", "uploadImportSource", "updateImport", "startImport", "resumeImport"].includes(operation) ? ["documents:write"] : []),
74
+ permission: webhook ? write ? "documents:write" : "documents:read" : operation === "getShapeManifest" || operation === "exportDatabase" || operation === "executeSql" || operation === "getCollectionTtl" ? "documents:read" : pathTemplate === ttl ? "documents:write" : pathTemplate.startsWith("/v1/platform/") ? "platform-admin" : !pathTemplate.startsWith("/v1/") ? "anonymous" : write ? "databases:write" : "databases:read",
75
+ additionalPermissions: Object.freeze(webhook && write ? ["documents:read"] : ["createImport", "uploadImportSource", "updateImport", "startImport", "resumeImport"].includes(operation) ? ["documents:write"] : []),
68
76
  requestMediaTypes: Object.freeze(noBody ? [] : operation === "uploadImportSource" ? ["application/octet-stream"] : method === "POST" || method === "PUT" || method === "PATCH" ? ["application/json"] : []),
69
77
  responseMediaType: status === 204 ? "" : operation === "exportDatabase" ? "application/x-ndjson" : "application/json", successStatuses: Object.freeze(operation === "readyz" || operation === "backupz" ? [200, 503] : operation === "applyIndexRecommendation" || operation === "createImport" ? [200, 201] : [status]),
70
- idempotencyKey: "none", replaySafe: !write, contract: "docs/terrabase/api.md", contractText: Object.freeze([]),
78
+ idempotencyKey: "none", replaySafe: !write, contract: webhook ? "docs/terrabase/change-webhooks.md" : "docs/terrabase/api.md", contractText: Object.freeze([]),
71
79
  });
72
80
  }
@@ -0,0 +1,132 @@
1
+ import { array, malformed, object, string } from "./decode.js";
2
+ import { requireName } from "./ids.js";
3
+ import { ExactDecimal, stringifyExactJson } from "./json.js";
4
+ /**
5
+ * @import { JsonValue } from "./json.js"
6
+ * @import { V1RequestOptions } from "./terrabase-v1.js"
7
+ * @import { V1Transport } from "./transport.js"
8
+ */
9
+
10
+ /**
11
+ * An argument: null, a finite number, an int64 `bigint` or ExactDecimal, a boolean (stored as 1 or 0) or a string.
12
+ * @typedef {null | boolean | number | bigint | ExactDecimal | string} V1SqlArgument
13
+ */
14
+ /** @typedef {readonly V1SqlArgument[] | Readonly<Record<string, V1SqlArgument>>} V1SqlArguments Positional array, or named values keyed without their `:`, `@` or `$` prefix. */
15
+ /**
16
+ * @typedef {object} V1SqlStatement
17
+ * @property {string} sql
18
+ * @property {readonly V1SqlArgument[] | undefined} [args] Positional arguments.
19
+ * @property {Readonly<Record<string, V1SqlArgument>> | undefined} [named] Named arguments, keys without their prefix.
20
+ */
21
+ /**
22
+ * A result value. Integers beyond 2^53 decode as ExactDecimal, keeping every digit; `BigInt(String(value))` converts one.
23
+ * @typedef {null | number | ExactDecimal | string} V1SqlValue
24
+ */
25
+ /** @typedef {{ readonly name: string; readonly decltype: string | null }} V1SqlColumn A column; `decltype` is null for an expression. */
26
+ /**
27
+ * @typedef {object} V1SqlExecuteResult
28
+ * @property {readonly V1SqlColumn[]} columns
29
+ * @property {readonly (readonly V1SqlValue[])[]} rows One value per column.
30
+ * @property {number} rowsAffected
31
+ * @property {number | ExactDecimal | null} lastInsertRowid
32
+ */
33
+
34
+ const maximumStatements = 256;
35
+ const maximumSqlLength = 16_384;
36
+
37
+ /** @param {unknown} value @param {string} where */
38
+ function requireArgument(value, where) {
39
+ if (value === null || typeof value === "boolean" || typeof value === "string" || value instanceof ExactDecimal) return;
40
+ if (typeof value === "number" && Number.isFinite(value)) return;
41
+ if (typeof value === "bigint" && BigInt.asIntN(64, value) === value) return;
42
+ throw new TypeError(`SQL ${where} is null, a finite number, an int64 bigint, a boolean or a string.`);
43
+ }
44
+
45
+ /** @param {unknown} statement @param {number} index */
46
+ function encodeStatement(statement, index) {
47
+ if (typeof statement !== "string" && (typeof statement !== "object" || statement === null || Array.isArray(statement)))
48
+ throw new TypeError(`SQL statement ${index} is a string or { sql, args, named }.`);
49
+ const { sql, args, named, ...extra } = /** @type {V1SqlStatement} */ (typeof statement === "string" ? { sql: statement } : statement);
50
+ for (const member of Object.keys(extra)) throw new TypeError(`Unknown SQL statement member ${member}.`);
51
+ if (typeof sql !== "string" || sql.length === 0 || sql.length > maximumSqlLength)
52
+ throw new TypeError(`SQL statement ${index} is a non-empty string of at most ${maximumSqlLength} UTF-16 code units.`);
53
+ /** @type {{ sql: string; args?: V1SqlArgument[]; named?: Record<string, V1SqlArgument> }} */
54
+ const encoded = { sql };
55
+ if (args !== undefined) {
56
+ if (!Array.isArray(args)) throw new TypeError(`SQL statement ${index} args is an array.`);
57
+ args.forEach((arg, position) => requireArgument(arg, `argument ${position}`));
58
+ encoded.args = [...args];
59
+ }
60
+ if (named !== undefined) {
61
+ if (named === null || typeof named !== "object" || Array.isArray(named)) throw new TypeError(`SQL statement ${index} named is an object.`);
62
+ for (const [name, value] of Object.entries(named)) {
63
+ if (/^[:@$]/u.test(name)) throw new TypeError(`SQL named argument ${name} is keyed without its prefix.`);
64
+ requireArgument(value, `named argument ${name}`);
65
+ }
66
+ encoded.named = { ...named };
67
+ }
68
+ return encoded;
69
+ }
70
+
71
+ /** @param {string} sql @param {V1SqlArguments | undefined} args @returns {V1SqlStatement} */
72
+ export function sqlStatement(sql, args) {
73
+ if (args === undefined) return { sql };
74
+ return Array.isArray(args) ? { sql, args } : { sql, named: /** @type {Readonly<Record<string, V1SqlArgument>>} */ (args) };
75
+ }
76
+
77
+ /** @param {JsonValue | undefined} value @returns {V1SqlValue} */
78
+ function decodeValue(value) {
79
+ if (value === null || typeof value === "number" || typeof value === "string" || value instanceof ExactDecimal) return value;
80
+ return malformed("SQL value");
81
+ }
82
+
83
+ /** @param {JsonValue | undefined} value @returns {V1SqlExecuteResult} */
84
+ function decodeResult(value) {
85
+ const raw = object(value, "SQL statement result");
86
+ const columns = array(raw.columns, "SQL columns").map(entry => {
87
+ const column = object(entry, "SQL column");
88
+ const decltype = column.decltype === null || column.decltype === undefined ? null : string(column.decltype, "SQL column decltype");
89
+ return Object.freeze({ name: string(column.name, "SQL column name"), decltype });
90
+ });
91
+ const rows = array(raw.rows, "SQL rows").map(entry => {
92
+ const row = array(entry, "SQL row").map(decodeValue);
93
+ if (row.length !== columns.length) malformed("SQL row width");
94
+ return Object.freeze(row);
95
+ });
96
+ const affected = raw.rows_affected;
97
+ if (typeof affected !== "number" || !Number.isSafeInteger(affected) || affected < 0) malformed("SQL rows_affected");
98
+ const rowid = raw.last_insert_rowid ?? null;
99
+ if (rowid !== null && !(rowid instanceof ExactDecimal) && !(typeof rowid === "number" && Number.isInteger(rowid))) malformed("SQL last_insert_rowid");
100
+ return Object.freeze({ columns: Object.freeze(columns), rows: Object.freeze(rows), rowsAffected: affected, lastInsertRowid: rowid });
101
+ }
102
+
103
+ /**
104
+ * Runs 1 to 256 statements as one transaction: all commit or none do. BEGIN, COMMIT and ROLLBACK are refused.
105
+ * A refused statement throws SqlError with its index.
106
+ * @param {V1Transport} transport
107
+ * @param {string} database
108
+ * @param {readonly (string | V1SqlStatement)[]} statements
109
+ * @param {V1RequestOptions} [options]
110
+ * @returns {Promise<readonly V1SqlExecuteResult[]>}
111
+ */
112
+ export async function executeSql(transport, database, statements, options = {}) {
113
+ if (!Array.isArray(statements) || statements.length === 0 || statements.length > maximumStatements)
114
+ throw new TypeError(`SQL batches hold 1 to ${maximumStatements} statements.`);
115
+ if (options === null || typeof options !== "object" || Array.isArray(options)) throw new TypeError("SQL request options must be an object.");
116
+ for (const member of Object.keys(options)) if (member !== "signal") throw new TypeError(`Unknown SQL request option ${member}.`);
117
+ const body = stringifyExactJson({ statements: statements.map(encodeStatement) }, "SQL statements");
118
+ const response = await transport.call("executeSql", { database: requireName("database", database) }, { body, signal: options.signal });
119
+ const results = array(object(response.value, "SQL execute response").results, "SQL results").map(decodeResult);
120
+ if (results.length !== statements.length) malformed("SQL result count");
121
+ return Object.freeze(results);
122
+ }
123
+
124
+ /**
125
+ * Rows as objects keyed by column name; a later duplicate column name wins.
126
+ * @template [T=Record<string, V1SqlValue>]
127
+ * @param {V1SqlExecuteResult} result
128
+ * @returns {T[]}
129
+ */
130
+ export function sqlRowObjects(result) {
131
+ return result.rows.map(row => /** @type {T} */ (Object.fromEntries(result.columns.map((column, index) => [column.name, row[index] ?? null]))));
132
+ }
@@ -11,6 +11,7 @@ import { readShapeManifest } from "./shape-manifest.js";
11
11
  import { V1ShapeSubscription } from "./shape-subscription.js";
12
12
  import { decryptSealedShapePage, readShapePage, readShapeKey } from "./shape-reader.js";
13
13
  import { runSql, checkSql } from "./sql.js";
14
+ import { executeSql, sqlRowObjects, sqlStatement } from "./sql-execute.js";
14
15
  import { streamChanges } from "./changes.js";
15
16
  import { exportDocuments } from "./export.js";
16
17
  import { V1Databases, getCollectionSchema, putCollectionSchema, deleteCollectionSchema, getCollectionTtl, putCollectionTtl, deleteCollectionTtl } from "./catalog.js";
@@ -22,6 +23,7 @@ import { V1ProjectUsageClient, V1DatabaseUsageClient } from "./usage.js";
22
23
  import { V1Indexes } from "./indexes.js";
23
24
  import { V1DatabaseSchemaClient } from "./database-schema.js";
24
25
  import { V1SavedQueries } from "./saved-queries.js";
26
+ import { V1ChangeWebhooks } from "./change-webhooks.js";
25
27
  /**
26
28
  * @import { V1Document, V1Durability, V1WriteResult } from "./decode.js"
27
29
  * @import { V1Filter, V1Path } from "./filter.js"
@@ -301,6 +303,8 @@ function encodeDocument(document) {
301
303
 
302
304
  /** A collection handle by name. It performs no I/O until an operation runs. */
303
305
  export class V1Collection {
306
+ /** @type {V1ChangeWebhooks} */
307
+ webhooks;
304
308
  /** @type {V1Transport} */
305
309
  #transport;
306
310
  /** @type {Readonly<Record<string, string>>} */
@@ -319,6 +323,7 @@ export class V1Collection {
319
323
  this.#transport = transport;
320
324
  this.database = requireName("database", database);
321
325
  this.name = requireName("collection", name);
326
+ this.webhooks = new V1ChangeWebhooks(transport, this.database, this.name);
322
327
  this.#params = Object.freeze({ database: this.database, collection: this.name });
323
328
  Object.freeze(this);
324
329
  }
@@ -852,6 +857,39 @@ export class V1Database {
852
857
  return checkSql(this.#transport, this.name, statement, options, "explainSql");
853
858
  }
854
859
 
860
+ /**
861
+ * Runs one relational SQL statement as its own transaction. Reads need documents:read; writes and DDL need documents:write.
862
+ * @param {string} sql
863
+ * @param {import("./sql-execute.js").V1SqlArguments} [args] Positional array, or named values keyed without their prefix.
864
+ * @param {V1RequestOptions} [options]
865
+ * @returns {Promise<import("./sql-execute.js").V1SqlExecuteResult>}
866
+ */
867
+ async execute(sql, args, options = {}) {
868
+ const [result] = await executeSql(this.#transport, this.name, [sqlStatement(sql, args)], options);
869
+ return /** @type {import("./sql-execute.js").V1SqlExecuteResult} */ (result);
870
+ }
871
+
872
+ /**
873
+ * Runs one statement and returns its rows as objects keyed by column name.
874
+ * @template [T=Record<string, import("./sql-execute.js").V1SqlValue>]
875
+ * @param {string} sql
876
+ * @param {import("./sql-execute.js").V1SqlArguments} [args]
877
+ * @param {V1RequestOptions} [options]
878
+ * @returns {Promise<T[]>}
879
+ */
880
+ async query(sql, args, options = {}) {
881
+ return sqlRowObjects(await this.execute(sql, args, options));
882
+ }
883
+
884
+ /**
885
+ * Runs 1 to 256 statements as one transaction: all commit or none do. A refused statement throws SqlError.
886
+ * @param {readonly (string | import("./sql-execute.js").V1SqlStatement)[]} statements
887
+ * @param {V1RequestOptions} [options]
888
+ */
889
+ batch(statements, options = {}) {
890
+ return executeSql(this.#transport, this.name, statements, options);
891
+ }
892
+
855
893
  /** @param {import("./changes.js").V1ChangeStreamRequest} [request] @param {V1RequestOptions} [options] */
856
894
  changes(request = {}, options = {}) {
857
895
  return streamChanges(this.#transport, this.name, request, options.signal);
package/types/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { V1Databases } from "./v1/catalog.js";
6
6
  export { V1SavedQueries } from "./v1/saved-queries.js";
7
7
  export { V1DatabaseSchemaClient } from "./v1/database-schema.js";
8
8
  export { V1Indexes } from "./v1/indexes.js";
9
+ export { V1ChangeWebhooks } from "./v1/change-webhooks.js";
9
10
  export { V1Imports } from "./v1/imports.js";
10
11
  export type V1ShapeSubscribeOptions = import("./v1/shape-subscription.js").V1ShapeSubscribeOptions;
11
12
  export type V1ShapeDocument = import("./v1/shape-subscription.js").V1ShapeDocument;
@@ -56,6 +57,12 @@ export type V1SqlRow = import("./v1/sql.js").V1SqlRow;
56
57
  export type V1SqlResult = import("./v1/sql.js").V1SqlResult;
57
58
  export type V1SqlIndexSuggestion = import("./v1/sql.js").V1SqlIndexSuggestion;
58
59
  export type V1SqlCheck = import("./v1/sql.js").V1SqlCheck;
60
+ export type V1SqlArgument = import("./v1/sql-execute.js").V1SqlArgument;
61
+ export type V1SqlArguments = import("./v1/sql-execute.js").V1SqlArguments;
62
+ export type V1SqlStatement = import("./v1/sql-execute.js").V1SqlStatement;
63
+ export type V1SqlValue = import("./v1/sql-execute.js").V1SqlValue;
64
+ export type V1SqlColumn = import("./v1/sql-execute.js").V1SqlColumn;
65
+ export type V1SqlExecuteResult = import("./v1/sql-execute.js").V1SqlExecuteResult;
59
66
  export type V1ChangeStreamRequest = import("./v1/changes.js").V1ChangeStreamRequest;
60
67
  export type V1DatabaseChange = import("./v1/changes.js").V1DatabaseChange;
61
68
  export type V1DatabaseChangeCursor = import("./v1/changes.js").V1DatabaseChangeCursor;
@@ -86,6 +93,10 @@ export type V1Analytics = import("./v1/usage.js").V1Analytics;
86
93
  export type V1Replication = import("./v1/usage.js").V1Replication;
87
94
  export type V1UsageRequest = import("./v1/usage.js").V1UsageRequest;
88
95
  export type V1AnalyticsRequest = import("./v1/usage.js").V1AnalyticsRequest;
96
+ export type V1ChangeWebhook = import("./v1/change-webhooks.js").V1ChangeWebhook;
97
+ export type V1CreatedChangeWebhook = import("./v1/change-webhooks.js").V1CreatedChangeWebhook;
98
+ export type V1ChangeWebhookDelivery = import("./v1/change-webhooks.js").V1ChangeWebhookDelivery;
99
+ export type V1ChangeWebhookListRequest = import("./v1/change-webhooks.js").V1ChangeWebhookListRequest;
89
100
  export type V1Backup = import("./v1/backups.js").V1Backup;
90
101
  export type V1BackupPage = import("./v1/backups.js").V1BackupPage;
91
102
  export type V1BackupPolicy = import("./v1/backups.js").V1BackupPolicy;
@@ -110,7 +121,7 @@ export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest
110
121
  export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
111
122
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
112
123
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
113
- export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
124
+ export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, SqlError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
114
125
  export { generateId, isValidId, requireId } from "./ids.js";
115
126
  export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
116
127
  export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
@@ -0,0 +1,59 @@
1
+ /** Collection change webhook registration and sanitized delivery history. Requests are never replayed. */
2
+ export class V1ChangeWebhooks {
3
+ /** @param {V1Transport} transport @param {string} database @param {string} collection */
4
+ constructor(transport: V1Transport, database: string, collection: string);
5
+ /**
6
+ * Registers from the current feed head. Requires documents:read and documents:write.
7
+ * Store secret immediately; it is non-enumerable to keep it out of JSON and ordinary diagnostics.
8
+ * @param {{ readonly url: string }} request @param {V1RequestOptions} [options] @returns {Promise<V1CreatedChangeWebhook>}
9
+ */
10
+ create(request: {
11
+ readonly url: string;
12
+ }, options?: V1RequestOptions): Promise<V1CreatedChangeWebhook>;
13
+ /**
14
+ * Requires documents:read. Ascending IDs; continue with the final ID until an empty array. No secrets.
15
+ * @param {V1ChangeWebhookListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<readonly V1ChangeWebhook[]>}
16
+ */
17
+ list(request?: V1ChangeWebhookListRequest, options?: V1RequestOptions): Promise<readonly V1ChangeWebhook[]>;
18
+ /**
19
+ * Requires documents:read and documents:write. An in-flight delivery may still complete. Unknown ID returns 404.
20
+ * @param {string} id @param {V1RequestOptions} [options] @returns {Promise<void>}
21
+ */
22
+ delete(id: string, options?: V1RequestOptions): Promise<void>;
23
+ /**
24
+ * Requires documents:read. Latest 100 retained records; paginate with the final ID until an empty array.
25
+ * @param {string} id @param {V1ChangeWebhookListRequest} [request] @param {V1RequestOptions} [options] @returns {Promise<readonly V1ChangeWebhookDelivery[]>}
26
+ */
27
+ deliveries(id: string, request?: V1ChangeWebhookListRequest, options?: V1RequestOptions): Promise<readonly V1ChangeWebhookDelivery[]>;
28
+ #private;
29
+ }
30
+ export type V1ChangeWebhook = ReturnType<typeof webhook>;
31
+ export type V1CreatedChangeWebhook = V1ChangeWebhook & {
32
+ readonly secret: string;
33
+ };
34
+ export type V1ChangeWebhookDelivery = ReturnType<typeof deliveries>[number];
35
+ export type V1ChangeWebhookListRequest = {
36
+ readonly after?: string;
37
+ readonly limit?: number;
38
+ };
39
+ import type { V1RequestOptions } from "./terrabase-v1.js";
40
+ import type { V1Transport } from "./transport.js";
41
+ /** @import { V1Transport } from "./transport.js" */
42
+ /** @import { V1RequestOptions } from "./terrabase-v1.js" */
43
+ declare const webhook: import("./management-decode.js").Decoder<Readonly<{
44
+ id: string;
45
+ url: string;
46
+ createdAt: string;
47
+ status: string;
48
+ }>>;
49
+ declare const deliveries: import("./management-decode.js").Decoder<readonly Readonly<{
50
+ id: string;
51
+ createdAt: string;
52
+ status: string;
53
+ attempts: number | import("./json.js").ExactDecimal;
54
+ lastAttemptAt: string | null;
55
+ nextAttemptAt: string | null;
56
+ httpStatus: number | import("./json.js").ExactDecimal | null;
57
+ errorCode: string | null;
58
+ }>[]>;
59
+ export {};
@@ -92,6 +92,9 @@ export const V1ErrorCode: Readonly<{
92
92
  internalError: "internal_error";
93
93
  capacity: "capacity";
94
94
  notModified: "not_modified";
95
+ sqlError: "sql_error";
96
+ sqlConflict: "sql_conflict";
97
+ sqlTooLarge: "sql_too_large";
95
98
  }>;
96
99
  /** `412 precondition_failed`: an If-Match, If-None-Match, assert or assert_count failed. */
97
100
  export class V1PreconditionFailedError extends TerraBaseV1Error {
@@ -105,6 +108,21 @@ export class V1ForbiddenScopeError extends TerraBaseV1Error {
105
108
  /** `404 not_found`: absent or deleted; `etag` carries a tombstone version. */
106
109
  export class V1NotFoundError extends TerraBaseV1Error {
107
110
  }
111
+ /**
112
+ * A refused relational SQL statement: `sql_error` (400), `forbidden_scope` (403, writes and DDL need
113
+ * documents:write), `sql_conflict` (409) or `sql_too_large` (413). The whole request rolled back.
114
+ */
115
+ export class SqlError extends TerraBaseV1Error {
116
+ /** @param {TerraBaseV1ErrorInit & { statement: number; sqliteCode?: string | undefined }} init */
117
+ constructor(init: TerraBaseV1ErrorInit & {
118
+ statement: number;
119
+ sqliteCode?: string | undefined;
120
+ });
121
+ /** @type {number} Zero-based index of the failing statement. */
122
+ statement: number;
123
+ /** @type {string | undefined} The SQLite error code, such as `SQLITE_CONSTRAINT_PRIMARYKEY`. */
124
+ sqliteCode: string | undefined;
125
+ }
108
126
  export type TerraBaseV1ErrorInit = {
109
127
  status: number;
110
128
  code: string;
@@ -1,3 +1,3 @@
1
1
  /** @type {readonly import("./routes.js").V1Route[]} */
2
2
  export const managementV1Routes: readonly import("./routes.js").V1Route[];
3
- export type ManagementOperation = "getShapeManifest" | "exportDatabase" | "putCollectionTtl" | "getCollectionTtl" | "deleteCollectionTtl" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery";
3
+ export type ManagementOperation = "getShapeManifest" | "exportDatabase" | "executeSql" | "putCollectionTtl" | "getCollectionTtl" | "deleteCollectionTtl" | "healthz" | "readyz" | "backupz" | "replicationHealth" | "listPlatformDatabases" | "listPlatformCollections" | "listPlatformDocuments" | "getPlatformDocument" | "createImport" | "getImport" | "uploadImportSource" | "updateImport" | "startImport" | "cancelImport" | "resumeImport" | "createBackup" | "listBackups" | "restoreBackup" | "getBackupPolicy" | "setBackupPolicy" | "getRestoreWindow" | "planPointInTime" | "restorePointInTime" | "getBackupDestination" | "setBackupDestination" | "deleteBackupDestination" | "getProjectUsage" | "getProjectAnalytics" | "getProjectReplication" | "getDatabaseUsage" | "getDatabaseAnalytics" | "getDatabaseReplication" | "createIndex" | "listIndexes" | "dropIndex" | "getAutoIndexPolicy" | "setAutoIndexPolicy" | "listIndexRecommendations" | "applyIndexRecommendation" | "getDatabaseSchema" | "inferCollectionSchema" | "declareCollectionSchema" | "listQueryShapes" | "createSavedQuery" | "listSavedQueries" | "getSavedQuery" | "updateSavedQuery" | "deleteSavedQuery" | "createChangeWebhook" | "listChangeWebhooks" | "deleteChangeWebhook" | "listChangeWebhookDeliveries";
@@ -0,0 +1,61 @@
1
+ /** @param {string} sql @param {V1SqlArguments | undefined} args @returns {V1SqlStatement} */
2
+ export function sqlStatement(sql: string, args: V1SqlArguments | undefined): V1SqlStatement;
3
+ /**
4
+ * Runs 1 to 256 statements as one transaction: all commit or none do. BEGIN, COMMIT and ROLLBACK are refused.
5
+ * A refused statement throws SqlError with its index.
6
+ * @param {V1Transport} transport
7
+ * @param {string} database
8
+ * @param {readonly (string | V1SqlStatement)[]} statements
9
+ * @param {V1RequestOptions} [options]
10
+ * @returns {Promise<readonly V1SqlExecuteResult[]>}
11
+ */
12
+ export function executeSql(transport: V1Transport, database: string, statements: readonly (string | V1SqlStatement)[], options?: V1RequestOptions): Promise<readonly V1SqlExecuteResult[]>;
13
+ /**
14
+ * Rows as objects keyed by column name; a later duplicate column name wins.
15
+ * @template [T=Record<string, V1SqlValue>]
16
+ * @param {V1SqlExecuteResult} result
17
+ * @returns {T[]}
18
+ */
19
+ export function sqlRowObjects<T = Record<string, V1SqlValue>>(result: V1SqlExecuteResult): T[];
20
+ /**
21
+ * An argument: null, a finite number, an int64 `bigint` or ExactDecimal, a boolean (stored as 1 or 0) or a string.
22
+ */
23
+ export type V1SqlArgument = null | boolean | number | bigint | ExactDecimal | string;
24
+ /**
25
+ * Positional array, or named values keyed without their `:`, `@` or `$` prefix.
26
+ */
27
+ export type V1SqlArguments = readonly V1SqlArgument[] | Readonly<Record<string, V1SqlArgument>>;
28
+ export type V1SqlStatement = {
29
+ sql: string;
30
+ /**
31
+ * Positional arguments.
32
+ */
33
+ args?: readonly V1SqlArgument[] | undefined;
34
+ /**
35
+ * Named arguments, keys without their prefix.
36
+ */
37
+ named?: Readonly<Record<string, V1SqlArgument>> | undefined;
38
+ };
39
+ /**
40
+ * A result value. Integers beyond 2^53 decode as ExactDecimal, keeping every digit; `BigInt(String(value))` converts one.
41
+ */
42
+ export type V1SqlValue = null | number | ExactDecimal | string;
43
+ /**
44
+ * A column; `decltype` is null for an expression.
45
+ */
46
+ export type V1SqlColumn = {
47
+ readonly name: string;
48
+ readonly decltype: string | null;
49
+ };
50
+ export type V1SqlExecuteResult = {
51
+ columns: readonly V1SqlColumn[];
52
+ /**
53
+ * One value per column.
54
+ */
55
+ rows: readonly (readonly V1SqlValue[])[];
56
+ rowsAffected: number;
57
+ lastInsertRowid: number | ExactDecimal | null;
58
+ };
59
+ import type { V1Transport } from "./transport.js";
60
+ import type { V1RequestOptions } from "./terrabase-v1.js";
61
+ import { ExactDecimal } from "./json.js";
@@ -22,6 +22,8 @@ export class V1Collection {
22
22
  * @param {string} name
23
23
  */
24
24
  constructor(transport: V1Transport, database: string, name: string);
25
+ /** @type {V1ChangeWebhooks} */
26
+ webhooks: V1ChangeWebhooks;
25
27
  /** @type {string} */
26
28
  name: string;
27
29
  /** @type {string} */
@@ -293,6 +295,29 @@ export class V1Database {
293
295
  checkSql(statement: string, options?: V1RequestOptions): Promise<import("./sql.js").V1SqlCheck>;
294
296
  /** @param {string} statement @param {V1RequestOptions} [options] */
295
297
  explainSql(statement: string, options?: V1RequestOptions): Promise<import("./sql.js").V1SqlCheck>;
298
+ /**
299
+ * Runs one relational SQL statement as its own transaction. Reads need documents:read; writes and DDL need documents:write.
300
+ * @param {string} sql
301
+ * @param {import("./sql-execute.js").V1SqlArguments} [args] Positional array, or named values keyed without their prefix.
302
+ * @param {V1RequestOptions} [options]
303
+ * @returns {Promise<import("./sql-execute.js").V1SqlExecuteResult>}
304
+ */
305
+ execute(sql: string, args?: import("./sql-execute.js").V1SqlArguments, options?: V1RequestOptions): Promise<import("./sql-execute.js").V1SqlExecuteResult>;
306
+ /**
307
+ * Runs one statement and returns its rows as objects keyed by column name.
308
+ * @template [T=Record<string, import("./sql-execute.js").V1SqlValue>]
309
+ * @param {string} sql
310
+ * @param {import("./sql-execute.js").V1SqlArguments} [args]
311
+ * @param {V1RequestOptions} [options]
312
+ * @returns {Promise<T[]>}
313
+ */
314
+ query<T = Record<string, import("./sql-execute.js").V1SqlValue>>(sql: string, args?: import("./sql-execute.js").V1SqlArguments, options?: V1RequestOptions): Promise<T[]>;
315
+ /**
316
+ * Runs 1 to 256 statements as one transaction: all commit or none do. A refused statement throws SqlError.
317
+ * @param {readonly (string | import("./sql-execute.js").V1SqlStatement)[]} statements
318
+ * @param {V1RequestOptions} [options]
319
+ */
320
+ batch(statements: readonly (string | import("./sql-execute.js").V1SqlStatement)[], options?: V1RequestOptions): Promise<readonly import("./sql-execute.js").V1SqlExecuteResult[]>;
296
321
  /** @param {import("./changes.js").V1ChangeStreamRequest} [request] @param {V1RequestOptions} [options] */
297
322
  changes(request?: import("./changes.js").V1ChangeStreamRequest, options?: V1RequestOptions): AsyncGenerator<import("./changes.js").V1DatabaseChange | import("./changes.js").V1DatabaseChangeCursor, void, undefined>;
298
323
  /**
@@ -483,6 +508,7 @@ export type V1TransactionResult = {
483
508
  };
484
509
  import type { JsonValue } from "./json.js";
485
510
  import type { TerraBaseV1ClientConfig } from "./transport.js";
511
+ import { V1ChangeWebhooks } from "./change-webhooks.js";
486
512
  import type { V1Document } from "./decode.js";
487
513
  import type { JsonInputObject } from "./json.js";
488
514
  import type { V1WriteResult } from "./decode.js";