@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 +9 -0
- package/README.md +33 -1
- package/package.json +1 -1
- package/sdk-public-v1-route-manifest.json +6 -1
- package/src/index.js +12 -0
- package/src/v1/change-webhooks.js +78 -0
- package/src/v1/errors.js +26 -0
- package/src/v1/management-routes.js +12 -4
- package/src/v1/sql-execute.js +132 -0
- package/src/v1/terrabase-v1.js +38 -0
- package/types/index.d.ts +12 -1
- package/types/v1/change-webhooks.d.ts +59 -0
- package/types/v1/errors.d.ts +18 -0
- package/types/v1/management-routes.d.ts +1 -1
- package/types/v1/sql-execute.d.ts +61 -0
- package/types/v1/terrabase-v1.d.ts +26 -0
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.
|
|
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
|
@@ -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
|
+
}
|
package/src/v1/terrabase-v1.js
CHANGED
|
@@ -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 {};
|
package/types/v1/errors.d.ts
CHANGED
|
@@ -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";
|