@terrascale/sdk 0.0.0-stage → 0.0.2

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