terrascale 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +143 -0
  3. package/package.json +159 -0
  4. package/sdk-current-contract.json +27 -0
  5. package/sdk-route-manifest.json +67 -0
  6. package/src/admin.js +9 -0
  7. package/src/better-auth.js +14 -0
  8. package/src/config.js +121 -0
  9. package/src/database-codec.js +845 -0
  10. package/src/database-types.js +237 -0
  11. package/src/database-view.js +422 -0
  12. package/src/database.js +420 -0
  13. package/src/discovery.js +374 -0
  14. package/src/http.js +887 -0
  15. package/src/index.js +79 -0
  16. package/src/local/authentication.js +47 -0
  17. package/src/local/better-auth.js +517 -0
  18. package/src/local/cli.js +51 -0
  19. package/src/local/context.js +23 -0
  20. package/src/local/environment.js +109 -0
  21. package/src/local/index.js +204 -0
  22. package/src/local/router.js +999 -0
  23. package/src/local/server.js +664 -0
  24. package/src/local/store.js +530 -0
  25. package/src/local/test-environment.js +74 -0
  26. package/src/management-contracts.js +72 -0
  27. package/src/management.js +12 -0
  28. package/src/native-origin.js +75 -0
  29. package/src/postgres.js +494 -0
  30. package/src/react/core.js +743 -0
  31. package/src/react/index.js +99 -0
  32. package/src/result.js +251 -0
  33. package/src/schema.js +366 -0
  34. package/src/sql.js +996 -0
  35. package/src/svelte/index.js +129 -0
  36. package/src/tanstack/index.js +511 -0
  37. package/src/ts-auth-discovery.js +190 -0
  38. package/src/ts-auth.js +3497 -0
  39. package/types/admin.d.ts +6 -0
  40. package/types/better-auth.d.ts +8 -0
  41. package/types/config.d.ts +58 -0
  42. package/types/database-codec.d.ts +111 -0
  43. package/types/database-types.d.ts +213 -0
  44. package/types/database-view.d.ts +183 -0
  45. package/types/database.d.ts +98 -0
  46. package/types/discovery.d.ts +114 -0
  47. package/types/http.d.ts +46 -0
  48. package/types/index.d.ts +52 -0
  49. package/types/local/authentication.d.ts +11 -0
  50. package/types/local/better-auth.d.ts +33 -0
  51. package/types/local/cli.d.ts +2 -0
  52. package/types/local/context.d.ts +14 -0
  53. package/types/local/environment.d.ts +23 -0
  54. package/types/local/index.d.ts +94 -0
  55. package/types/local/router.d.ts +66 -0
  56. package/types/local/server.d.ts +54 -0
  57. package/types/local/store.d.ts +106 -0
  58. package/types/local/test-environment.d.ts +25 -0
  59. package/types/management-contracts.d.ts +44 -0
  60. package/types/management.d.ts +6 -0
  61. package/types/native-origin.d.ts +23 -0
  62. package/types/postgres.d.ts +123 -0
  63. package/types/react/core.d.ts +366 -0
  64. package/types/react/index.d.ts +54 -0
  65. package/types/result.d.ts +161 -0
  66. package/types/schema.d.ts +145 -0
  67. package/types/sql.d.ts +288 -0
  68. package/types/svelte/index.d.ts +81 -0
  69. package/types/tanstack/index.d.ts +165 -0
  70. package/types/ts-auth-discovery.d.ts +11 -0
  71. package/types/ts-auth.d.ts +1826 -0
@@ -0,0 +1,75 @@
1
+ /** Internal admission policy shared by native clients and qualification configuration. */
2
+ export const terraScaleServiceDomain = "terrascale.tech";
3
+ const dataApiDomain = `api.${terraScaleServiceDomain}`;
4
+
5
+ const reservedRegions = new Set([
6
+ "API", "ATLAS", "AUTH", "SYNC", "STATUS", "WWW", "APP", "DASH",
7
+ "DOCS", "GIT", "CDN", "ADMIN", "MCP", "MAIL",
8
+ ]);
9
+
10
+ /**
11
+ * @param {unknown} value
12
+ * @returns {value is string}
13
+ */
14
+ function isNativeRegion(value) {
15
+ return typeof value === "string" && /^[A-Z0-9][A-Z0-9-]{0,15}$/u.test(value) && !value.endsWith("-") && !value.includes("--") && !reservedRegions.has(value);
16
+ }
17
+
18
+ /**
19
+ * @param {unknown} value
20
+ * @returns {string}
21
+ */
22
+ export function requireNativeRegion(value) {
23
+ if (!isNativeRegion(value)) throw new TypeError("Invalid native region.");
24
+ return value;
25
+ }
26
+
27
+ /**
28
+ * @param {unknown} code
29
+ * @returns {string}
30
+ */
31
+ export function nativeRegionalOrigin(code) {
32
+ return `https://${requireNativeRegion(code).toLowerCase()}.${dataApiDomain}`;
33
+ }
34
+
35
+ /**
36
+ * Hosted identity refuses database API origins, without using native admission.
37
+ * @param {string} hostname
38
+ * @returns {boolean}
39
+ */
40
+ export function isDataApiHostname(hostname) {
41
+ const canonical = hostname.toLowerCase().replace(/\.+$/u, "");
42
+ return canonical === dataApiDomain || canonical.endsWith(`.${dataApiDomain}`);
43
+ }
44
+
45
+ /**
46
+ * @param {string} raw
47
+ * @returns {string}
48
+ */
49
+ export function nativeServiceOrigin(raw) {
50
+ const url = new URL(raw);
51
+ if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== "/") {
52
+ throw new TypeError("Service origin must be bare and contain no credentials.");
53
+ }
54
+ const authority = /** @type {string} */ (raw.slice(raw.indexOf("//") + 2).split(/[/?#]/u, 1)[0]);
55
+ const rawHostname = authority.startsWith("[") ? authority.slice(0, authority.indexOf("]") + 1) : /** @type {string} */ (authority.split(":", 1)[0]);
56
+ // Independent origins may use ASCII case aliases, but must not be silently
57
+ // redirected through percent decoding or IDNA hostname normalization.
58
+ if (!/^[\x00-\x7f]+$/u.test(rawHostname) || rawHostname.toLowerCase() !== url.hostname) {
59
+ throw new TypeError("Service origins must use a canonical ASCII hostname.");
60
+ }
61
+ const hostname = url.hostname.replace(/\.+$/u, "");
62
+ const regionalSuffix = `.${terraScaleServiceDomain}`;
63
+ if (hostname === terraScaleServiceDomain || hostname.endsWith(regionalSuffix)) {
64
+ const code = hostname.endsWith(`.${dataApiDomain}`) ? hostname.slice(0, -(dataApiDomain.length + 1)).toUpperCase() : "";
65
+ if (!isNativeRegion(code) || url.origin !== nativeRegionalOrigin(code) || url.hostname !== hostname || authority !== hostname) {
66
+ throw new TypeError("TerraScale service origins must use a canonical regional hostname.");
67
+ }
68
+ }
69
+ if (url.hostname !== hostname) throw new TypeError("Service origins must use a canonical hostname.");
70
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(hostname) || hostname.endsWith(".localhost");
71
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
72
+ throw new TypeError("Service origins must use HTTPS, except loopback development.");
73
+ }
74
+ return url.origin;
75
+ }
@@ -0,0 +1,494 @@
1
+ /** Node-only PostgreSQL client for the signed INT8/TEXT point SQL listener. */
2
+ import { lookup } from "node:dns/promises";
3
+ import { isIP } from "node:net";
4
+ import { X509Certificate } from "node:crypto";
5
+ import { checkServerIdentity, createSecureContext, TLSSocket } from "node:tls";
6
+ /**
7
+ * @import {
8
+ * Client as DriverClient,
9
+ * ClientConfig,
10
+ * DatabaseError,
11
+ * FieldDef,
12
+ * QueryArrayConfig,
13
+ * QueryArrayResult,
14
+ * } from "pg"
15
+ */
16
+
17
+ const MAX_BYTES = 16 * 1024;
18
+ const MIN_INT8 = -(1n << 63n);
19
+ const MAX_INT8 = (1n << 63n) - 1n;
20
+
21
+ /** @typedef {bigint | string} PostgresParameter */
22
+ /**
23
+ * @typedef {{
24
+ * readonly statement: string;
25
+ * readonly parameters: readonly unknown[];
26
+ * }} PostgresQuery
27
+ */
28
+ /**
29
+ * @typedef {{
30
+ * readonly statement: string;
31
+ * readonly parameters: readonly PostgresParameter[];
32
+ * }} PostgresBoundQuery
33
+ */
34
+ /**
35
+ * @typedef {{
36
+ * readonly host: string;
37
+ * readonly port: number;
38
+ * readonly tlsServerName: string;
39
+ * readonly caCertificatePem: string;
40
+ * readonly database: string;
41
+ * readonly keyId: string;
42
+ * readonly password: string;
43
+ * readonly connectTimeoutMs?: number;
44
+ * }} PostgresConfig
45
+ */
46
+ /**
47
+ * @typedef {{
48
+ * readonly name: string;
49
+ * readonly oid: 20 | 25;
50
+ * }} PostgresColumn
51
+ */
52
+ /**
53
+ * @typedef {{
54
+ * readonly command: string;
55
+ * readonly affectedRows: number;
56
+ * readonly columns: readonly PostgresColumn[];
57
+ * readonly rows: readonly (readonly PostgresParameter[])[];
58
+ * }} PostgresResult
59
+ */
60
+
61
+ /** Diagnostics retain SQLSTATE, but never driver messages, SQL or passwords. */
62
+ export class PostgresError extends Error {
63
+ /**
64
+ * @override
65
+ * @readonly
66
+ */
67
+ name = /** @type {const} */ ("PostgresError");
68
+ /**
69
+ * @readonly
70
+ * @type {"configuration" | "query" | "connection" | "database" | "protocol"}
71
+ */
72
+ kind;
73
+ /**
74
+ * @readonly
75
+ * @type {string | undefined}
76
+ */
77
+ sqlState;
78
+
79
+ /**
80
+ * @param {"configuration" | "query" | "connection" | "database" | "protocol"} kind
81
+ * @param {string} [sqlState]
82
+ */
83
+ constructor(kind, sqlState) {
84
+ super(
85
+ `terrascale PostgreSQL ${kind} failure${sqlState === undefined ? "" : ` (${sqlState})`}`,
86
+ );
87
+ this.kind = kind;
88
+ this.sqlState = sqlState;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @param {string} value
94
+ * @returns {boolean}
95
+ */
96
+ function validText(value) {
97
+ if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_BYTES)
98
+ return false;
99
+ for (let index = 0; index < value.length; index++) {
100
+ const code = value.charCodeAt(index);
101
+ if (code >= 0xd800 && code <= 0xdbff) {
102
+ const next = value.charCodeAt(++index);
103
+ if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
104
+ } else if (code >= 0xdc00 && code <= 0xdfff) return false;
105
+ }
106
+ return true;
107
+ }
108
+
109
+ /**
110
+ * Validate and clone a builder descriptor without converting or interpolating values.
111
+ *
112
+ * @param {string} statement
113
+ * @param {readonly unknown[]} [parameters]
114
+ * @returns {PostgresBoundQuery}
115
+ */
116
+ export function postgresQuery(statement, parameters = []) {
117
+ if (
118
+ typeof statement !== "string" ||
119
+ statement.trim() === "" ||
120
+ !validText(statement) ||
121
+ !Array.isArray(parameters) ||
122
+ parameters.length > 4
123
+ )
124
+ throw new PostgresError("query");
125
+ /** @type {PostgresParameter[]} */
126
+ const values = parameters.map((value) => {
127
+ if (typeof value === "bigint" && value >= MIN_INT8 && value <= MAX_INT8)
128
+ return value;
129
+ if (typeof value === "string" && validText(value)) return value;
130
+ throw new PostgresError("query");
131
+ });
132
+ return Object.freeze({ statement, parameters: Object.freeze(values) });
133
+ }
134
+
135
+ /**
136
+ * @param {string} value
137
+ * @param {number} bytes
138
+ * @returns {boolean}
139
+ */
140
+ function canonicalBase64(value, bytes) {
141
+ return (
142
+ /^[A-Za-z0-9_-]+$/.test(value) &&
143
+ Buffer.from(value, "base64url").length === bytes &&
144
+ Buffer.from(value, "base64url").toString("base64url") === value
145
+ );
146
+ }
147
+
148
+ /**
149
+ * @param {PostgresConfig} config
150
+ * @returns {PostgresConfig & { readonly connectTimeoutMs: number }}
151
+ */
152
+ function configuration(config) {
153
+ const timeout = config.connectTimeoutMs ?? 5000;
154
+ const parts =
155
+ typeof config.password === "string" ? config.password.split(".") : [];
156
+ if (
157
+ typeof config.host !== "string" ||
158
+ config.host === "" ||
159
+ config.host.length > 253 ||
160
+ /[\s,\/\0]/.test(config.host) ||
161
+ !Number.isInteger(config.port) ||
162
+ config.port < 1 ||
163
+ config.port > 65535 ||
164
+ typeof config.tlsServerName !== "string" ||
165
+ config.tlsServerName === "" ||
166
+ /[\s,\/\0]/.test(config.tlsServerName) ||
167
+ !Number.isInteger(timeout) ||
168
+ timeout < 1 ||
169
+ timeout > 10000 ||
170
+ !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(config.database) ||
171
+ !canonicalBase64(config.keyId, 16) ||
172
+ parts.length !== 3 ||
173
+ parts[0] !== "tbk1" ||
174
+ parts[1] !== config.keyId ||
175
+ !canonicalBase64(parts[2] ?? "", 32) ||
176
+ typeof config.caCertificatePem !== "string" ||
177
+ config.caCertificatePem.length > 64 * 1024
178
+ ) {
179
+ throw new PostgresError("configuration");
180
+ }
181
+ try {
182
+ const certificates = config.caCertificatePem.match(
183
+ /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g,
184
+ );
185
+ if (certificates === null || certificates.length === 0)
186
+ throw new PostgresError("configuration");
187
+ for (const certificate of certificates) new X509Certificate(certificate);
188
+ createSecureContext({
189
+ ca: config.caCertificatePem,
190
+ minVersion: "TLSv1.3",
191
+ maxVersion: "TLSv1.3",
192
+ });
193
+ } catch {
194
+ throw new PostgresError("configuration");
195
+ }
196
+ return Object.freeze({ ...config, connectTimeoutMs: timeout });
197
+ }
198
+
199
+ /**
200
+ * @param {unknown} error
201
+ * @param {typeof DatabaseError} [databaseError]
202
+ * @returns {PostgresError}
203
+ */
204
+ function failure(error, databaseError) {
205
+ if (error instanceof PostgresError) return error;
206
+ if (
207
+ databaseError !== undefined &&
208
+ error instanceof databaseError &&
209
+ typeof error.code === "string" &&
210
+ /^[0-9A-Z]{5}$/.test(error.code)
211
+ ) {
212
+ return new PostgresError("database", error.code);
213
+ }
214
+ return new PostgresError("connection");
215
+ }
216
+
217
+ /**
218
+ * @param {readonly FieldDef[]} fields
219
+ * @returns {readonly PostgresColumn[]}
220
+ */
221
+ function columns(fields) {
222
+ return Object.freeze(
223
+ fields.map((field) => {
224
+ if (field.dataTypeID !== 20 && field.dataTypeID !== 25)
225
+ throw new PostgresError("protocol");
226
+ return Object.freeze({ name: field.name, oid: field.dataTypeID });
227
+ }),
228
+ );
229
+ }
230
+
231
+ /** One driver connection; no automatic reconnect, mutation retry or callback replay. */
232
+ export class PostgresClient {
233
+ /** @type {DriverClient} */
234
+ #driver;
235
+ /** @type {typeof DatabaseError} */
236
+ #databaseError;
237
+ #closed = false;
238
+ /**
239
+ * @private
240
+ * @param {DriverClient} driver
241
+ * @param {typeof DatabaseError} databaseError
242
+ */
243
+ constructor(driver, databaseError) {
244
+ this.#driver = driver;
245
+ this.#databaseError = databaseError;
246
+ driver.on("error", () => {
247
+ this.#closed = true;
248
+ });
249
+ }
250
+
251
+ /**
252
+ * @param {PostgresConfig} input
253
+ * @returns {Promise<PostgresClient>}
254
+ */
255
+ static async connect(input) {
256
+ const config = configuration(input);
257
+ // pg treats empty options/replication as absent and reads these variables.
258
+ // Reject them before loading the driver or opening a socket.
259
+ if (
260
+ process.env["NODE_PG_FORCE_NATIVE"] ||
261
+ process.env["PGOPTIONS"] ||
262
+ process.env["PGREPLICATION"]
263
+ ) {
264
+ throw new PostgresError("configuration");
265
+ }
266
+ const started = Date.now();
267
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
268
+ let timer;
269
+ try {
270
+ // pg overwrites SNI for DNS hosts. Resolve only the configured address,
271
+ // then send the separately pinned TLS name through its SSL options.
272
+ const address = isIP(config.host)
273
+ ? config.host
274
+ : await Promise.race([
275
+ lookup(config.host).then((result) => result.address),
276
+ /** @type {Promise<never>} */ (
277
+ new Promise((_resolve, reject) => {
278
+ timer = setTimeout(() => {
279
+ reject(new PostgresError("connection"));
280
+ }, config.connectTimeoutMs);
281
+ })
282
+ ),
283
+ ]);
284
+ if (timer !== undefined) clearTimeout(timer);
285
+ const remaining = config.connectTimeoutMs - (Date.now() - started);
286
+ if (remaining <= 0) throw new PostgresError("connection");
287
+ const { default: pg } = await import("pg");
288
+ // Pinned pg 8.23 emits sslconnect immediately after creating TLSSocket.
289
+ // Its Client sends startup on that event, so fence the event itself:
290
+ // waiting for Client.connect() would disclose credentials too early.
291
+ class VerifiedTlsConnection extends pg.Connection {
292
+ #tlsStarted = false;
293
+
294
+ /**
295
+ * @override
296
+ * @param {string | symbol} event
297
+ * @param {...unknown} args
298
+ * @returns {boolean}
299
+ */
300
+ emit(event, ...args) {
301
+ if (event !== "sslconnect") return super.emit(event, ...args);
302
+ const hasListeners = this.listenerCount(event) > 0;
303
+ if (this.#tlsStarted) return hasListeners;
304
+ this.#tlsStarted = true;
305
+ const socket = this.stream;
306
+ if (!(socket instanceof TLSSocket)) {
307
+ socket.destroy(new PostgresError("protocol"));
308
+ return hasListeners;
309
+ }
310
+ /** @returns {void} */
311
+ const cleanup = () => {
312
+ socket.off("secureConnect", verified);
313
+ socket.off("error", cleanup);
314
+ socket.off("close", cleanup);
315
+ };
316
+ /** @returns {void} */
317
+ const verified = () => {
318
+ cleanup();
319
+ if (socket.destroyed) return;
320
+ if (!socket.authorized || socket.alpnProtocol !== "postgresql") {
321
+ socket.destroy(new PostgresError("protocol"));
322
+ return;
323
+ }
324
+ super.emit(event, ...args);
325
+ };
326
+ socket.once("secureConnect", verified);
327
+ socket.once("error", cleanup);
328
+ socket.once("close", cleanup);
329
+ return hasListeners;
330
+ }
331
+ }
332
+ /**
333
+ * @type {ClientConfig & {
334
+ * binary: false;
335
+ * client_encoding: string;
336
+ * sslnegotiation: "postgres";
337
+ * }}
338
+ */
339
+ const driverConfig = {
340
+ host: address,
341
+ port: config.port,
342
+ database: config.database,
343
+ user: config.keyId,
344
+ password: config.password,
345
+ connectionTimeoutMillis: remaining,
346
+ application_name: "terrascale-postgres",
347
+ options: "",
348
+ binary: false,
349
+ client_encoding: "UTF8",
350
+ sslnegotiation: "postgres",
351
+ ssl: {
352
+ ca: config.caCertificatePem,
353
+ minVersion: "TLSv1.3",
354
+ maxVersion: "TLSv1.3",
355
+ rejectUnauthorized: true,
356
+ ALPNProtocols: ["postgresql"],
357
+ servername: config.tlsServerName,
358
+ checkServerIdentity: (_host, certificate) =>
359
+ checkServerIdentity(config.tlsServerName, certificate),
360
+ },
361
+ types: {
362
+ getTypeParser: (
363
+ /** @type {number} */ oid,
364
+ /** @type {string | undefined} */ format,
365
+ ) => {
366
+ if (format === "binary")
367
+ return () => {
368
+ throw new PostgresError("protocol");
369
+ };
370
+ if (oid === 20)
371
+ return (/** @type {string} */ value) => BigInt(value);
372
+ return (/** @type {string} */ value) => value;
373
+ },
374
+ },
375
+ };
376
+ // Connection's pinned runtime option uses camelCase; the driver's public
377
+ // ClientConfig type does not yet declare the supported connection seam.
378
+ /** @type {ClientConfig & { sslNegotiation: "postgres" }} */
379
+ const connectionConfig = {
380
+ ssl: driverConfig.ssl,
381
+ sslNegotiation: "postgres",
382
+ };
383
+ /**
384
+ * @type {ClientConfig & {
385
+ * connection: InstanceType<typeof VerifiedTlsConnection>;
386
+ * }}
387
+ */
388
+ const clientConfig = {
389
+ ...driverConfig,
390
+ connection: new VerifiedTlsConnection(connectionConfig),
391
+ };
392
+ const driver = new pg.Client(clientConfig);
393
+ const client = new PostgresClient(driver, pg.DatabaseError);
394
+ try {
395
+ await driver.connect();
396
+ return client;
397
+ } catch (error) {
398
+ await driver.end().catch(() => undefined);
399
+ throw failure(error, pg.DatabaseError);
400
+ }
401
+ } catch (error) {
402
+ throw failure(error);
403
+ } finally {
404
+ if (timer !== undefined) clearTimeout(timer);
405
+ }
406
+ }
407
+
408
+ /**
409
+ * @param {PostgresQuery} input
410
+ * @returns {Promise<PostgresResult>}
411
+ */
412
+ async query(input) {
413
+ const query = postgresQuery(input.statement, input.parameters);
414
+ if (this.#closed) throw new PostgresError("connection");
415
+ try {
416
+ /** @type {QueryArrayConfig<string[]> & { binary: false }} */
417
+ const queryConfig = {
418
+ text: query.statement,
419
+ values: query.parameters.map((value) =>
420
+ typeof value === "bigint" ? value.toString() : value,
421
+ ),
422
+ rowMode: "array",
423
+ binary: false,
424
+ };
425
+ const result = /** @type {QueryArrayResult<unknown[]>} */ (
426
+ await this.#driver.query(queryConfig)
427
+ );
428
+ const schema = columns(result.fields);
429
+ if (
430
+ result.rows.length > 1 ||
431
+ ![
432
+ "SELECT",
433
+ "INSERT",
434
+ "UPDATE",
435
+ "DELETE",
436
+ "BEGIN",
437
+ "COMMIT",
438
+ "ROLLBACK",
439
+ "SET",
440
+ ].includes(result.command)
441
+ ) {
442
+ throw new PostgresError("protocol");
443
+ }
444
+ const rows = result.rows.map((row) => {
445
+ if (row.length !== schema.length) throw new PostgresError("protocol");
446
+ return Object.freeze(
447
+ row.map((value, index) => {
448
+ if (
449
+ schema[index]?.oid === 20 &&
450
+ typeof value === "bigint" &&
451
+ value >= MIN_INT8 &&
452
+ value <= MAX_INT8
453
+ )
454
+ return value;
455
+ if (
456
+ schema[index]?.oid === 25 &&
457
+ typeof value === "string" &&
458
+ validText(value)
459
+ )
460
+ return value;
461
+ throw new PostgresError("protocol");
462
+ }),
463
+ );
464
+ });
465
+ const affectedRows = result.rowCount ?? 0;
466
+ if (
467
+ !Number.isSafeInteger(affectedRows) ||
468
+ affectedRows < 0 ||
469
+ affectedRows > 2
470
+ )
471
+ throw new PostgresError("protocol");
472
+ return Object.freeze({
473
+ command: result.command,
474
+ affectedRows,
475
+ columns: schema,
476
+ rows: Object.freeze(rows),
477
+ });
478
+ } catch (error) {
479
+ throw failure(error, this.#databaseError);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * @returns {Promise<void>}
485
+ */
486
+ async close() {
487
+ this.#closed = true;
488
+ try {
489
+ await this.#driver.end();
490
+ } catch (error) {
491
+ throw failure(error, this.#databaseError);
492
+ }
493
+ }
494
+ }