@onreza/sqlx-js 0.4.0 → 0.5.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.
- package/README.md +85 -34
- package/ROADMAP.md +1 -3
- package/dist/bin/sqlx-js.js +91 -20
- package/dist/src/artifacts.d.ts +9 -0
- package/dist/src/artifacts.js +26 -0
- package/dist/src/cache.d.ts +17 -0
- package/dist/src/cache.js +83 -1
- package/dist/src/codegen.js +14 -2
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +196 -0
- package/dist/src/commands/init.js +6 -10
- package/dist/src/commands/migrate.js +7 -22
- package/dist/src/commands/pgschema.d.ts +6 -0
- package/dist/src/commands/pgschema.js +30 -0
- package/dist/src/commands/prepare.d.ts +40 -6
- package/dist/src/commands/prepare.js +341 -63
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +141 -11
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/postgres-runtime.d.ts +3 -0
- package/dist/src/postgres-runtime.js +66 -29
- package/dist/src/runtime.d.ts +36 -3
- package/dist/src/runtime.js +91 -22
- package/dist/src/scan/scanner.d.ts +9 -2
- package/dist/src/scan/scanner.js +79 -28
- package/dist/src/typed.d.ts +10 -0
- package/package.json +6 -5
package/dist/src/config.js
CHANGED
|
@@ -1,18 +1,148 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
export function defineConfig(config) {
|
|
7
|
+
return config;
|
|
8
|
+
}
|
|
9
|
+
export function loadRootEnv(root) {
|
|
10
|
+
const path = join(root, ".env");
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return undefined;
|
|
13
|
+
const parsed = parseEnv(readFileSync(path, "utf8"));
|
|
14
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
15
|
+
if (process.env[key] === undefined)
|
|
16
|
+
process.env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
export function configPath(root) {
|
|
21
|
+
for (const name of ["sqlx-js.config.mts", "sqlx-js.config.ts", "sqlx-js.config.mjs", "sqlx-js.config.js"]) {
|
|
5
22
|
const p = join(root, name);
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
if (existsSync(p))
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export async function loadConfig(root) {
|
|
29
|
+
const path = configPath(root);
|
|
30
|
+
if (!path)
|
|
31
|
+
return {};
|
|
32
|
+
const url = pathToFileURL(path);
|
|
33
|
+
url.searchParams.set("mtime", String(statSync(path).mtimeMs));
|
|
34
|
+
const mod = await import(url.href);
|
|
35
|
+
if (!("default" in mod)) {
|
|
36
|
+
throw new Error(`sqlx-js: ${path} must default-export a config object`);
|
|
37
|
+
}
|
|
38
|
+
return validateConfig(mod.default, path);
|
|
39
|
+
}
|
|
40
|
+
function validateStringRecord(value, name, path) {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
42
|
+
throw new Error(`sqlx-js: ${path} ${name} must be an object of string values`);
|
|
43
|
+
}
|
|
44
|
+
for (const [key, item] of Object.entries(value)) {
|
|
45
|
+
if (typeof item !== "string") {
|
|
46
|
+
throw new Error(`sqlx-js: ${path} ${name}.${key} must be a string`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validateStringArray(value, name, path) {
|
|
51
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
52
|
+
throw new Error(`sqlx-js: ${path} ${name} must be an array of strings`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateConfig(value, path) {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
throw new Error(`sqlx-js: ${path} must default-export a config object`);
|
|
58
|
+
}
|
|
59
|
+
const config = value;
|
|
60
|
+
if (config.jsonbTypes !== undefined)
|
|
61
|
+
validateStringRecord(config.jsonbTypes, "jsonbTypes", path);
|
|
62
|
+
if (config.customTypes !== undefined)
|
|
63
|
+
validateStringRecord(config.customTypes, "customTypes", path);
|
|
64
|
+
if (config.scan !== undefined) {
|
|
65
|
+
if (!config.scan || typeof config.scan !== "object" || Array.isArray(config.scan)) {
|
|
66
|
+
throw new Error(`sqlx-js: ${path} scan must be an object`);
|
|
12
67
|
}
|
|
13
|
-
|
|
68
|
+
const scan = config.scan;
|
|
69
|
+
if (scan.include !== undefined)
|
|
70
|
+
validateStringArray(scan.include, "scan.include", path);
|
|
71
|
+
if (scan.exclude !== undefined)
|
|
72
|
+
validateStringArray(scan.exclude, "scan.exclude", path);
|
|
14
73
|
}
|
|
15
|
-
|
|
74
|
+
if (config.schema !== undefined) {
|
|
75
|
+
if (!config.schema || typeof config.schema !== "object" || Array.isArray(config.schema)) {
|
|
76
|
+
throw new Error(`sqlx-js: ${path} schema must be an object`);
|
|
77
|
+
}
|
|
78
|
+
const schema = config.schema;
|
|
79
|
+
if (schema.provider !== undefined && schema.provider !== "builtin" && schema.provider !== "pgschema") {
|
|
80
|
+
throw new Error(`sqlx-js: ${path} schema.provider must be builtin or pgschema`);
|
|
81
|
+
}
|
|
82
|
+
for (const key of ["file", "command"]) {
|
|
83
|
+
if (schema[key] !== undefined && typeof schema[key] !== "string") {
|
|
84
|
+
throw new Error(`sqlx-js: ${path} schema.${key} must be a string`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (schema.schemas !== undefined)
|
|
88
|
+
validateStringArray(schema.schemas, "schema.schemas", path);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
export function prepareConfigHash(cfg) {
|
|
93
|
+
const value = stableValue({
|
|
94
|
+
jsonbTypes: cfg.jsonbTypes ?? {},
|
|
95
|
+
customTypes: cfg.customTypes ?? {},
|
|
96
|
+
});
|
|
97
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
export function configHash(cfg) {
|
|
100
|
+
return createHash("sha256").update(JSON.stringify(stableValue(cfg))).digest("hex").slice(0, 16);
|
|
101
|
+
}
|
|
102
|
+
function stableValue(value) {
|
|
103
|
+
if (Array.isArray(value))
|
|
104
|
+
return value.map(stableValue);
|
|
105
|
+
if (!value || typeof value !== "object")
|
|
106
|
+
return value;
|
|
107
|
+
return Object.fromEntries(Object.entries(value)
|
|
108
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
109
|
+
.map(([key, item]) => [key, stableValue(item)]));
|
|
110
|
+
}
|
|
111
|
+
export async function loadConfigInfo(root) {
|
|
112
|
+
const path = configPath(root);
|
|
113
|
+
if (!path)
|
|
114
|
+
return { config: {} };
|
|
115
|
+
return { config: await loadConfig(root), path };
|
|
116
|
+
}
|
|
117
|
+
export function assertSupportedRuntime() {
|
|
118
|
+
const bun = process.versions.bun;
|
|
119
|
+
if (bun) {
|
|
120
|
+
if (majorMinorLessThan(bun, 1, 3)) {
|
|
121
|
+
throw new Error(`sqlx-js requires Bun >=1.3, current ${bun}`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const node = process.versions.node;
|
|
126
|
+
if (majorMinorLessThan(node, 24, 0)) {
|
|
127
|
+
throw new Error(`sqlx-js requires Node.js >=24, current ${node}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function majorMinorLessThan(version, minMajor, minMinor) {
|
|
131
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
132
|
+
return major < minMajor || (major === minMajor && minor < minMinor);
|
|
133
|
+
}
|
|
134
|
+
export function runtimeVersion() {
|
|
135
|
+
if (process.versions.bun)
|
|
136
|
+
return { runtime: "bun", version: process.versions.bun };
|
|
137
|
+
return { runtime: "node", version: process.versions.node };
|
|
138
|
+
}
|
|
139
|
+
export function nativeTypeScriptEnabled() {
|
|
140
|
+
if (process.versions.bun)
|
|
141
|
+
return true;
|
|
142
|
+
return process.features.typescript;
|
|
143
|
+
}
|
|
144
|
+
export function envFilePath(root) {
|
|
145
|
+
return join(root, ".env");
|
|
16
146
|
}
|
|
17
147
|
export function lookupJsonbType(cfg, schema, table, column) {
|
|
18
148
|
const types = cfg.jsonbTypes;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -18,11 +18,13 @@ export type JsonInputObject = {
|
|
|
18
18
|
readonly [key: string]: JsonInputValue | undefined;
|
|
19
19
|
};
|
|
20
20
|
export type JsonInputArray = readonly JsonInputValue[];
|
|
21
|
-
export
|
|
21
|
+
export { defineConfig } from "./config.js";
|
|
22
|
+
export type { ScanConfig, SqlxJsConfig } from "./config.js";
|
|
22
23
|
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
23
24
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
24
25
|
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
25
26
|
export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook } from "./runtime.js";
|
|
27
|
+
export type { ExecuteResult, JsonParameter, PgArrayParameter } from "./runtime.js";
|
|
26
28
|
export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
|
|
27
29
|
export type TypedFile = TypedFileFor<KnownFileQueries>;
|
|
28
30
|
export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
|
|
@@ -34,4 +36,4 @@ export declare const getClient: typeof rt.getClient;
|
|
|
34
36
|
export declare const setClient: typeof rt.setClient;
|
|
35
37
|
export declare const createClient: typeof rt.createClient;
|
|
36
38
|
export declare const close: typeof rt.close;
|
|
37
|
-
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
|
|
39
|
+
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as rt from "./postgres-runtime.js";
|
|
2
|
+
export { defineConfig } from "./config.js";
|
|
2
3
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
3
4
|
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
4
5
|
export const sql = rt.sql;
|
|
@@ -7,4 +8,4 @@ export const getClient = rt.getClient;
|
|
|
7
8
|
export const setClient = rt.setClient;
|
|
8
9
|
export const createClient = rt.createClient;
|
|
9
10
|
export const close = rt.close;
|
|
10
|
-
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
|
|
11
|
+
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
|
|
@@ -7,11 +7,14 @@ export type PostgresOptions = postgres.Options<Record<string, postgres.PostgresT
|
|
|
7
7
|
export type CreateClientOptions = PostgresOptions & {
|
|
8
8
|
onQuery?: OnQueryHook;
|
|
9
9
|
statementTimeoutMs?: number;
|
|
10
|
+
fileRoot?: string;
|
|
10
11
|
};
|
|
11
12
|
export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
|
|
12
13
|
export declare function getClient(): PostgresClient;
|
|
13
14
|
export declare function setClient(client: PostgresClient, options?: {
|
|
14
15
|
onQuery?: OnQueryHook;
|
|
16
|
+
prepare?: boolean;
|
|
17
|
+
fileRoot?: string;
|
|
15
18
|
}): void;
|
|
16
19
|
export declare function close(): Promise<void>;
|
|
17
20
|
export declare const sql: import("./runtime.js").SqlRoot;
|
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createSqlRuntime, encodePgArrayLiteral, isPrimitiveArrayElement, parameterKind, parsePgArrayLiteral, } from "./runtime.js";
|
|
3
4
|
import { arrayElementOid, builtinArrayOids } from "./pg/oids.js";
|
|
4
5
|
const HOOKS = Symbol.for("sqlx-js.hooks");
|
|
6
|
+
function resolvedFileRoot(value) {
|
|
7
|
+
return resolve(value ?? process.env.SQLX_JS_FILE_ROOT ?? process.cwd());
|
|
8
|
+
}
|
|
5
9
|
class PostgresRuntimeClient {
|
|
6
10
|
client;
|
|
7
11
|
onQuery;
|
|
8
|
-
|
|
12
|
+
prepare;
|
|
13
|
+
fileRoot;
|
|
14
|
+
constructor(client, onQuery, prepare = true, fileRoot = resolvedFileRoot()) {
|
|
9
15
|
this.client = client;
|
|
10
16
|
this.onQuery = onQuery;
|
|
17
|
+
this.prepare = prepare;
|
|
18
|
+
this.fileRoot = fileRoot;
|
|
11
19
|
}
|
|
12
20
|
async query(query, params) {
|
|
13
|
-
return await this.client.unsafe(query, params, { prepare:
|
|
21
|
+
return await this.client.unsafe(query, params, { prepare: this.prepare });
|
|
14
22
|
}
|
|
15
23
|
transformParam(param) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
return this.client.
|
|
19
|
-
|
|
24
|
+
const kind = parameterKind(param);
|
|
25
|
+
if (kind === "json")
|
|
26
|
+
return this.client.json(param.value);
|
|
27
|
+
if (kind === "array") {
|
|
28
|
+
const value = [...param.value];
|
|
29
|
+
if (value.every(isPrimitiveArrayElement))
|
|
30
|
+
return encodePgArrayLiteral(value);
|
|
31
|
+
if (value.every((item) => item === null || parameterKind(item) === "json")) {
|
|
32
|
+
return this.client.array(value, 3807);
|
|
33
|
+
}
|
|
34
|
+
return this.client.array(value);
|
|
35
|
+
}
|
|
36
|
+
return param;
|
|
20
37
|
}
|
|
21
38
|
async transaction(fn) {
|
|
22
39
|
if (!("begin" in this.client))
|
|
23
40
|
throw new Error("sqlx-js.transaction: nested transactions are not supported");
|
|
24
41
|
return await this.client.begin(async (tx) => {
|
|
25
|
-
return await fn(new PostgresRuntimeClient(tx, this.onQuery));
|
|
42
|
+
return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.prepare, this.fileRoot));
|
|
26
43
|
});
|
|
27
44
|
}
|
|
28
45
|
async close() {
|
|
@@ -74,6 +91,7 @@ const STRING_ARRAY_ELEMENT_OIDS = new Set([
|
|
|
74
91
|
4451,
|
|
75
92
|
4536,
|
|
76
93
|
]);
|
|
94
|
+
const JSON_ELEMENT_OIDS = new Set([114, 3802]);
|
|
77
95
|
function parseSimpleArrayElement(oid) {
|
|
78
96
|
switch (oid) {
|
|
79
97
|
case 16:
|
|
@@ -90,28 +108,21 @@ function parseSimpleArrayElement(oid) {
|
|
|
90
108
|
return STRING_ARRAY_ELEMENT_OIDS.has(oid) ? (value) => value : undefined;
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
|
-
function postgresNativeArrayElementOid(value) {
|
|
94
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
95
|
-
return undefined;
|
|
96
|
-
let oid;
|
|
97
|
-
for (const item of value) {
|
|
98
|
-
if (item === null)
|
|
99
|
-
continue;
|
|
100
|
-
const itemOid = item instanceof Uint8Array ? 17 : undefined;
|
|
101
|
-
if (itemOid === undefined)
|
|
102
|
-
return undefined;
|
|
103
|
-
if (oid !== undefined && oid !== itemOid)
|
|
104
|
-
return undefined;
|
|
105
|
-
oid = itemOid;
|
|
106
|
-
}
|
|
107
|
-
return oid;
|
|
108
|
-
}
|
|
109
111
|
function postgresTypes() {
|
|
110
112
|
const types = { bigint: postgres.BigInt };
|
|
111
113
|
for (const oid of builtinArrayOids()) {
|
|
112
114
|
const elementOid = arrayElementOid(oid);
|
|
113
115
|
if (elementOid === undefined)
|
|
114
116
|
continue;
|
|
117
|
+
if (JSON_ELEMENT_OIDS.has(elementOid)) {
|
|
118
|
+
types[`array_${oid}`] = {
|
|
119
|
+
to: oid,
|
|
120
|
+
from: [oid],
|
|
121
|
+
serialize: (value) => Array.isArray(value) ? encodePgArrayLiteral(value) : String(value),
|
|
122
|
+
parse: (value) => parsePgArrayLiteral(value, JSON.parse),
|
|
123
|
+
};
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
115
126
|
const parseElement = parseSimpleArrayElement(elementOid);
|
|
116
127
|
if (!parseElement)
|
|
117
128
|
continue;
|
|
@@ -124,10 +135,30 @@ function postgresTypes() {
|
|
|
124
135
|
}
|
|
125
136
|
return types;
|
|
126
137
|
}
|
|
138
|
+
function typeOids(value) {
|
|
139
|
+
if (value === null || value === undefined)
|
|
140
|
+
return [];
|
|
141
|
+
return Array.isArray(value) ? value : [value];
|
|
142
|
+
}
|
|
143
|
+
function installJsonArrayCodecs(client) {
|
|
144
|
+
const options = client.options;
|
|
145
|
+
options.parsers ??= {};
|
|
146
|
+
options.serializers ??= {};
|
|
147
|
+
const configured = Object.values(options.types ?? {});
|
|
148
|
+
for (const oid of [199, 3807]) {
|
|
149
|
+
const hasParser = configured.some((type) => typeOids(type.from).includes(oid));
|
|
150
|
+
const hasSerializer = configured.some((type) => type.to === oid);
|
|
151
|
+
if (!hasParser)
|
|
152
|
+
options.parsers[oid] = (value) => parsePgArrayLiteral(value, JSON.parse);
|
|
153
|
+
if (!hasSerializer) {
|
|
154
|
+
options.serializers[oid] = (value) => Array.isArray(value) ? encodePgArrayLiteral(value) : String(value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
127
158
|
export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
128
159
|
if (!url)
|
|
129
160
|
throw new Error("sqlx-js: DATABASE_URL is not set");
|
|
130
|
-
const { onQuery, statementTimeoutMs, ...pgOptions } = options;
|
|
161
|
+
const { onQuery, statementTimeoutMs, fileRoot, ...pgOptions } = options;
|
|
131
162
|
const connection = statementTimeoutMs !== undefined
|
|
132
163
|
? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
|
|
133
164
|
: pgOptions.connection;
|
|
@@ -136,12 +167,17 @@ export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
|
136
167
|
...(connection ? { connection } : {}),
|
|
137
168
|
types: { ...postgresTypes(), ...(pgOptions.types ?? {}) },
|
|
138
169
|
});
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
client[HOOKS] = {
|
|
171
|
+
onQuery,
|
|
172
|
+
prepare: pgOptions.prepare ?? true,
|
|
173
|
+
fileRoot: resolvedFileRoot(fileRoot),
|
|
174
|
+
};
|
|
141
175
|
return client;
|
|
142
176
|
}
|
|
143
177
|
function createDefaultClient() {
|
|
144
|
-
|
|
178
|
+
const client = createClient();
|
|
179
|
+
const attached = client[HOOKS];
|
|
180
|
+
return new PostgresRuntimeClient(client, attached.onQuery, attached.prepare, attached.fileRoot);
|
|
145
181
|
}
|
|
146
182
|
function getRuntimeClient() {
|
|
147
183
|
defaultClient ??= createDefaultClient();
|
|
@@ -151,8 +187,9 @@ export function getClient() {
|
|
|
151
187
|
return getRuntimeClient().client;
|
|
152
188
|
}
|
|
153
189
|
export function setClient(client, options) {
|
|
190
|
+
installJsonArrayCodecs(client);
|
|
154
191
|
const attached = client[HOOKS];
|
|
155
|
-
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery);
|
|
192
|
+
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot));
|
|
156
193
|
}
|
|
157
194
|
export async function close() {
|
|
158
195
|
if (defaultClient) {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -7,19 +7,47 @@ export type OnQueryEvent = {
|
|
|
7
7
|
error?: unknown;
|
|
8
8
|
};
|
|
9
9
|
export type OnQueryHook = (event: OnQueryEvent) => void;
|
|
10
|
+
export type RuntimeQueryResult = unknown[] & {
|
|
11
|
+
count?: number | null;
|
|
12
|
+
command?: string | null;
|
|
13
|
+
};
|
|
10
14
|
export type RuntimeClient = {
|
|
11
|
-
query: (query: string, params: unknown[]) => Promise<
|
|
15
|
+
query: (query: string, params: unknown[]) => Promise<RuntimeQueryResult>;
|
|
12
16
|
transformParam?: (param: unknown) => unknown;
|
|
13
17
|
transaction: <R>(fn: (client: RuntimeClient) => Promise<R>) => Promise<R>;
|
|
14
18
|
close: () => Promise<void>;
|
|
15
19
|
onQuery?: OnQueryHook;
|
|
20
|
+
fileRoot?: string;
|
|
16
21
|
};
|
|
17
22
|
type AnyFn = (...args: unknown[]) => Promise<unknown[]>;
|
|
18
23
|
type AnyOneFn = (...args: unknown[]) => Promise<unknown>;
|
|
19
24
|
type AnyOptionalFn = (...args: unknown[]) => Promise<unknown | null>;
|
|
25
|
+
type AnyExecuteFn = (...args: unknown[]) => Promise<ExecuteResult>;
|
|
20
26
|
type IdentifierFn = (...parts: string[]) => string;
|
|
27
|
+
declare const PARAMETER_KIND: unique symbol;
|
|
28
|
+
export type JsonParameter<T = JsonInputValue> = {
|
|
29
|
+
readonly [PARAMETER_KIND]: "json";
|
|
30
|
+
readonly value: T;
|
|
31
|
+
};
|
|
32
|
+
export type PgArrayParameter<T = unknown> = {
|
|
33
|
+
readonly [PARAMETER_KIND]: "array";
|
|
34
|
+
readonly value: readonly (T | null)[];
|
|
35
|
+
};
|
|
36
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
37
|
+
export type JsonInputValue = JsonPrimitive | JsonInputObject | JsonInputArray;
|
|
38
|
+
export type JsonInputObject = {
|
|
39
|
+
readonly [key: string]: JsonInputValue | undefined;
|
|
40
|
+
};
|
|
41
|
+
export type JsonInputArray = readonly JsonInputValue[];
|
|
42
|
+
export type ExecuteResult = {
|
|
43
|
+
rowCount: number;
|
|
44
|
+
command: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function json<T extends JsonInputValue>(value: T): JsonParameter<T>;
|
|
47
|
+
export declare function array<T>(value: readonly (T | null)[]): PgArrayParameter<T>;
|
|
48
|
+
export declare function parameterKind(value: unknown): "json" | "array" | undefined;
|
|
21
49
|
declare function renameRows(rows: unknown[]): unknown[];
|
|
22
|
-
declare function isPrimitiveArrayElement(v: unknown): boolean;
|
|
50
|
+
export declare function isPrimitiveArrayElement(v: unknown): boolean;
|
|
23
51
|
export declare function encodePgArrayLiteral(arr: unknown[]): string;
|
|
24
52
|
type PgArrayValue<T> = T | null | PgArrayValue<T>[];
|
|
25
53
|
export declare function parsePgArrayLiteral<T = string>(input: string, parseElement?: (value: string) => T): PgArrayValue<T>[];
|
|
@@ -40,21 +68,26 @@ export declare const _internal: {
|
|
|
40
68
|
loadSqlFile: typeof loadSqlFile;
|
|
41
69
|
buildSetTransaction: typeof buildSetTransaction;
|
|
42
70
|
clearIdentifierCache: typeof clearIdentifierCache;
|
|
71
|
+
parameterKind: typeof parameterKind;
|
|
43
72
|
toPgError: typeof toPgError;
|
|
44
73
|
};
|
|
45
|
-
declare function loadSqlFile(path: string): string;
|
|
74
|
+
declare function loadSqlFile(path: string, fileRoot?: string): string;
|
|
46
75
|
export declare function clearSqlFileCache(): void;
|
|
47
76
|
declare function clearIdentifierCache(): void;
|
|
48
77
|
export declare function id(...parts: string[]): string;
|
|
49
78
|
type FileCallable = AnyFn & {
|
|
50
79
|
one: AnyOneFn;
|
|
51
80
|
optional: AnyOptionalFn;
|
|
81
|
+
execute: AnyExecuteFn;
|
|
52
82
|
};
|
|
53
83
|
type SqlCallable = AnyFn & {
|
|
54
84
|
file: FileCallable;
|
|
55
85
|
one: AnyOneFn;
|
|
56
86
|
optional: AnyOptionalFn;
|
|
87
|
+
execute: AnyExecuteFn;
|
|
57
88
|
id: IdentifierFn;
|
|
89
|
+
json: typeof json;
|
|
90
|
+
array: typeof array;
|
|
58
91
|
};
|
|
59
92
|
export type TransactionOptions = {
|
|
60
93
|
isolation?: "read uncommitted" | "read committed" | "repeatable read" | "serializable";
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { PgClient, parseDatabaseUrl, PgError } from "./pg/wire.js";
|
|
4
4
|
import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./commands/migrate.js";
|
|
5
|
+
const PARAMETER_KIND = Symbol("sqlx-js.parameter");
|
|
6
|
+
export function json(value) {
|
|
7
|
+
return { [PARAMETER_KIND]: "json", value };
|
|
8
|
+
}
|
|
9
|
+
export function array(value) {
|
|
10
|
+
return { [PARAMETER_KIND]: "array", value };
|
|
11
|
+
}
|
|
12
|
+
export function parameterKind(value) {
|
|
13
|
+
if (!value || typeof value !== "object")
|
|
14
|
+
return undefined;
|
|
15
|
+
return value[PARAMETER_KIND];
|
|
16
|
+
}
|
|
5
17
|
const SUFFIX = /[!?]$/;
|
|
6
18
|
function renameRows(rows) {
|
|
7
19
|
if (rows.length === 0)
|
|
@@ -28,9 +40,11 @@ function renameRows(rows) {
|
|
|
28
40
|
}
|
|
29
41
|
return out;
|
|
30
42
|
}
|
|
31
|
-
function isPrimitiveArrayElement(v) {
|
|
43
|
+
export function isPrimitiveArrayElement(v) {
|
|
32
44
|
if (v === null || v === undefined)
|
|
33
45
|
return true;
|
|
46
|
+
if (v instanceof Date || v instanceof Uint8Array)
|
|
47
|
+
return true;
|
|
34
48
|
const t = typeof v;
|
|
35
49
|
return t === "string" || t === "number" || t === "bigint" || t === "boolean";
|
|
36
50
|
}
|
|
@@ -44,6 +58,18 @@ export function encodePgArrayLiteral(arr) {
|
|
|
44
58
|
parts.push("NULL");
|
|
45
59
|
continue;
|
|
46
60
|
}
|
|
61
|
+
if (parameterKind(v) === "json") {
|
|
62
|
+
parts.push(quoteArrayElement(JSON.stringify(v.value)));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (v instanceof Date) {
|
|
66
|
+
parts.push(quoteArrayElement(v.toISOString()));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (v instanceof Uint8Array) {
|
|
70
|
+
parts.push(quoteArrayElement(`\\x${Buffer.from(v).toString("hex")}`));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
47
73
|
if (typeof v === "bigint") {
|
|
48
74
|
parts.push(v.toString());
|
|
49
75
|
continue;
|
|
@@ -126,13 +152,12 @@ export function parsePgArrayLiteral(input, parseElement = (value) => value) {
|
|
|
126
152
|
return parsed;
|
|
127
153
|
}
|
|
128
154
|
export function encodeParam(p) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return encodePgArrayLiteral(p);
|
|
155
|
+
const kind = parameterKind(p);
|
|
156
|
+
if (kind === "json")
|
|
157
|
+
return JSON.stringify(p.value);
|
|
158
|
+
if (kind === "array")
|
|
159
|
+
return encodePgArrayLiteral([...p.value]);
|
|
160
|
+
return p;
|
|
136
161
|
}
|
|
137
162
|
export class NoRowsError extends Error {
|
|
138
163
|
constructor(message = "expected exactly 1 row, got 0") {
|
|
@@ -213,16 +238,17 @@ export const _internal = {
|
|
|
213
238
|
loadSqlFile,
|
|
214
239
|
buildSetTransaction,
|
|
215
240
|
clearIdentifierCache,
|
|
241
|
+
parameterKind,
|
|
216
242
|
toPgError,
|
|
217
243
|
};
|
|
218
|
-
async function
|
|
244
|
+
async function runRawQuery(client, query, params) {
|
|
219
245
|
const encoded = params.length === 0
|
|
220
246
|
? params
|
|
221
247
|
: params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
|
|
222
248
|
const onQuery = client.onQuery;
|
|
223
249
|
if (!onQuery) {
|
|
224
250
|
try {
|
|
225
|
-
return
|
|
251
|
+
return await client.query(query, encoded);
|
|
226
252
|
}
|
|
227
253
|
catch (e) {
|
|
228
254
|
throw toPgError(e) ?? e;
|
|
@@ -230,9 +256,14 @@ async function runQuery(client, query, params) {
|
|
|
230
256
|
}
|
|
231
257
|
const start = performance.now();
|
|
232
258
|
try {
|
|
233
|
-
const
|
|
234
|
-
onQuery({
|
|
235
|
-
|
|
259
|
+
const result = await client.query(query, encoded);
|
|
260
|
+
onQuery({
|
|
261
|
+
query,
|
|
262
|
+
params,
|
|
263
|
+
durationMs: performance.now() - start,
|
|
264
|
+
rowCount: result.count ?? result.length,
|
|
265
|
+
});
|
|
266
|
+
return result;
|
|
236
267
|
}
|
|
237
268
|
catch (e) {
|
|
238
269
|
const error = toPgError(e) ?? e;
|
|
@@ -240,6 +271,16 @@ async function runQuery(client, query, params) {
|
|
|
240
271
|
throw error;
|
|
241
272
|
}
|
|
242
273
|
}
|
|
274
|
+
async function runQuery(client, query, params) {
|
|
275
|
+
return renameRows(await runRawQuery(client, query, params));
|
|
276
|
+
}
|
|
277
|
+
async function runExecute(client, query, params) {
|
|
278
|
+
const result = await runRawQuery(client, query, params);
|
|
279
|
+
return {
|
|
280
|
+
rowCount: result.count ?? result.length,
|
|
281
|
+
command: result.command ?? "",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
243
284
|
async function runOne(client, query, params) {
|
|
244
285
|
const rows = await runQuery(client, query, params);
|
|
245
286
|
if (rows.length === 1)
|
|
@@ -257,8 +298,16 @@ async function runOptional(client, query, params) {
|
|
|
257
298
|
throw new TooManyRowsError(rows.length, "0 or 1");
|
|
258
299
|
}
|
|
259
300
|
const sqlFileCache = new Map();
|
|
260
|
-
function loadSqlFile(path) {
|
|
261
|
-
const
|
|
301
|
+
function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd()) {
|
|
302
|
+
const root = resolve(fileRoot);
|
|
303
|
+
if (isAbsolute(path)) {
|
|
304
|
+
throw new Error(`sqlx-js.sql.file: path must be relative to fileRoot: ${path}`);
|
|
305
|
+
}
|
|
306
|
+
const full = resolve(root, path);
|
|
307
|
+
const rel = relative(root, full);
|
|
308
|
+
if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
|
|
309
|
+
throw new Error(`sqlx-js.sql.file: path escapes fileRoot: ${path}`);
|
|
310
|
+
}
|
|
262
311
|
try {
|
|
263
312
|
const st = statSync(full);
|
|
264
313
|
const cached = sqlFileCache.get(full);
|
|
@@ -393,13 +442,16 @@ function makeBoundCallable(client) {
|
|
|
393
442
|
return runQuery(client, query, params);
|
|
394
443
|
});
|
|
395
444
|
const file = (async (path, ...params) => {
|
|
396
|
-
return runQuery(client, loadSqlFile(path), params);
|
|
445
|
+
return runQuery(client, loadSqlFile(path, client.fileRoot), params);
|
|
397
446
|
});
|
|
398
447
|
file.one = (async (path, ...params) => {
|
|
399
|
-
return runOne(client, loadSqlFile(path), params);
|
|
448
|
+
return runOne(client, loadSqlFile(path, client.fileRoot), params);
|
|
400
449
|
});
|
|
401
450
|
file.optional = (async (path, ...params) => {
|
|
402
|
-
return runOptional(client, loadSqlFile(path), params);
|
|
451
|
+
return runOptional(client, loadSqlFile(path, client.fileRoot), params);
|
|
452
|
+
});
|
|
453
|
+
file.execute = (async (path, ...params) => {
|
|
454
|
+
return runExecute(client, loadSqlFile(path, client.fileRoot), params);
|
|
403
455
|
});
|
|
404
456
|
fn.file = file;
|
|
405
457
|
fn.one = (async (query, ...params) => {
|
|
@@ -408,7 +460,12 @@ function makeBoundCallable(client) {
|
|
|
408
460
|
fn.optional = (async (query, ...params) => {
|
|
409
461
|
return runOptional(client, query, params);
|
|
410
462
|
});
|
|
463
|
+
fn.execute = (async (query, ...params) => {
|
|
464
|
+
return runExecute(client, query, params);
|
|
465
|
+
});
|
|
411
466
|
fn.id = id;
|
|
467
|
+
fn.json = json;
|
|
468
|
+
fn.array = array;
|
|
412
469
|
return fn;
|
|
413
470
|
}
|
|
414
471
|
function buildSetTransaction(opts) {
|
|
@@ -428,13 +485,20 @@ export function createSqlRuntime(getClient) {
|
|
|
428
485
|
return runQuery(getClient(), query, params);
|
|
429
486
|
});
|
|
430
487
|
const rootFile = (async (path, ...params) => {
|
|
431
|
-
|
|
488
|
+
const client = getClient();
|
|
489
|
+
return runQuery(client, loadSqlFile(path, client.fileRoot), params);
|
|
432
490
|
});
|
|
433
491
|
rootFile.one = (async (path, ...params) => {
|
|
434
|
-
|
|
492
|
+
const client = getClient();
|
|
493
|
+
return runOne(client, loadSqlFile(path, client.fileRoot), params);
|
|
435
494
|
});
|
|
436
495
|
rootFile.optional = (async (path, ...params) => {
|
|
437
|
-
|
|
496
|
+
const client = getClient();
|
|
497
|
+
return runOptional(client, loadSqlFile(path, client.fileRoot), params);
|
|
498
|
+
});
|
|
499
|
+
rootFile.execute = (async (path, ...params) => {
|
|
500
|
+
const client = getClient();
|
|
501
|
+
return runExecute(client, loadSqlFile(path, client.fileRoot), params);
|
|
438
502
|
});
|
|
439
503
|
root.file = rootFile;
|
|
440
504
|
root.one = (async (query, ...params) => {
|
|
@@ -443,7 +507,12 @@ export function createSqlRuntime(getClient) {
|
|
|
443
507
|
root.optional = (async (query, ...params) => {
|
|
444
508
|
return runOptional(getClient(), query, params);
|
|
445
509
|
});
|
|
510
|
+
root.execute = (async (query, ...params) => {
|
|
511
|
+
return runExecute(getClient(), query, params);
|
|
512
|
+
});
|
|
446
513
|
root.id = id;
|
|
514
|
+
root.json = json;
|
|
515
|
+
root.array = array;
|
|
447
516
|
root.transaction = (async (fnOrOpts, maybeFn) => {
|
|
448
517
|
const c = getClient();
|
|
449
518
|
let opts = {};
|