@onreza/sqlx-js 0.4.0 → 0.6.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 +139 -48
- package/ROADMAP.md +1 -3
- package/dist/bin/sqlx-js-diagnostics.d.ts +2 -0
- package/dist/bin/sqlx-js-diagnostics.js +96 -0
- package/dist/bin/sqlx-js.js +337 -89
- 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 +93 -13
- package/dist/src/commands/migrate.d.ts +2 -101
- package/dist/src/commands/migrate.js +11 -566
- package/dist/src/commands/pgschema.d.ts +6 -0
- package/dist/src/commands/pgschema.js +30 -0
- package/dist/src/commands/prepare.d.ts +41 -6
- package/dist/src/commands/prepare.js +440 -63
- package/dist/src/commands/schema.js +1 -1
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +22 -0
- package/dist/src/config.js +149 -11
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/migration-core.d.ts +157 -0
- package/dist/src/migration-core.js +578 -0
- 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 +92 -23
- package/dist/src/scan/scanner.d.ts +10 -3
- package/dist/src/scan/scanner.js +83 -32
- package/dist/src/type-inspection.d.ts +1 -0
- package/dist/src/type-inspection.js +20 -0
- package/dist/src/typed.d.ts +10 -0
- package/package.json +11 -6
|
@@ -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
|
-
import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./
|
|
4
|
+
import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./migration-core.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 = {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ScanConfig } from "../config.js";
|
|
1
2
|
export type QueryCallSite = {
|
|
2
3
|
file: string;
|
|
3
4
|
line: number;
|
|
@@ -7,6 +8,12 @@ export type QueryCallSite = {
|
|
|
7
8
|
kind: "inline" | "file";
|
|
8
9
|
sqlFilePath?: string;
|
|
9
10
|
};
|
|
10
|
-
export declare
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export declare class ScanError extends Error {
|
|
12
|
+
readonly file: string;
|
|
13
|
+
readonly line: number;
|
|
14
|
+
readonly column: number;
|
|
15
|
+
constructor(file: string, line: number, column: number, message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare function findSourceFiles(root: string, scan?: ScanConfig): string[];
|
|
18
|
+
export declare function scanFile(absPath: string, root: string, modules?: readonly string[]): QueryCallSite[];
|
|
19
|
+
export declare function scanProject(root: string, scan?: ScanConfig): QueryCallSite[];
|
package/dist/src/scan/scanner.js
CHANGED
|
@@ -1,25 +1,67 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { existsSync, readFileSync
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
export class ScanError extends Error {
|
|
5
|
+
file;
|
|
6
|
+
line;
|
|
7
|
+
column;
|
|
8
|
+
constructor(file, line, column, message) {
|
|
9
|
+
super(`sqlx-js: ${file}:${line}:${column} — ${message}`);
|
|
10
|
+
this.file = file;
|
|
11
|
+
this.line = line;
|
|
12
|
+
this.column = column;
|
|
13
|
+
this.name = "ScanError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const DEFAULT_EXCLUDES = [
|
|
17
|
+
"**/node_modules/**",
|
|
18
|
+
"**/.git/**",
|
|
19
|
+
"**/.sqlx-js/**",
|
|
20
|
+
"**/dist/**",
|
|
21
|
+
"**/build/**",
|
|
22
|
+
"**/.next/**",
|
|
23
|
+
];
|
|
24
|
+
const DEFAULT_SQLX_MODULES = ["@onreza/sqlx-js"];
|
|
6
25
|
const EXT = /\.(ts|tsx|mts|cts)$/;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
const TS_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
|
|
27
|
+
function formatConfigError(error) {
|
|
28
|
+
return ts.flattenDiagnosticMessageText(error.messageText, "\n");
|
|
29
|
+
}
|
|
30
|
+
function collectTsconfigFiles(configPath, out, visited) {
|
|
31
|
+
const resolved = resolve(configPath);
|
|
32
|
+
if (visited.has(resolved))
|
|
33
|
+
return;
|
|
34
|
+
visited.add(resolved);
|
|
35
|
+
const read = ts.readConfigFile(resolved, ts.sys.readFile);
|
|
36
|
+
if (read.error)
|
|
37
|
+
throw new Error(`sqlx-js scan: ${resolved}: ${formatConfigError(read.error)}`);
|
|
38
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, resolve(resolved, ".."), undefined, resolved);
|
|
39
|
+
if (parsed.errors.length > 0) {
|
|
40
|
+
throw new Error(`sqlx-js scan: ${resolved}: ${parsed.errors.map(formatConfigError).join("; ")}`);
|
|
41
|
+
}
|
|
42
|
+
for (const file of parsed.fileNames) {
|
|
43
|
+
if (EXT.test(file))
|
|
44
|
+
out.add(resolve(file));
|
|
45
|
+
}
|
|
46
|
+
for (const reference of parsed.projectReferences ?? []) {
|
|
47
|
+
collectTsconfigFiles(ts.resolveProjectReferencePath(reference), out, visited);
|
|
17
48
|
}
|
|
18
49
|
}
|
|
19
|
-
export function findSourceFiles(root) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
export function findSourceFiles(root, scan = {}) {
|
|
51
|
+
const excludes = [...DEFAULT_EXCLUDES, ...(scan.exclude ?? [])];
|
|
52
|
+
if (scan.include !== undefined) {
|
|
53
|
+
if (scan.include.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
return ts.sys.readDirectory(root, TS_EXTENSIONS, excludes, scan.include).map((file) => resolve(file)).sort();
|
|
56
|
+
}
|
|
57
|
+
const configPath = join(root, "tsconfig.json");
|
|
58
|
+
if (!ts.sys.fileExists(configPath)) {
|
|
59
|
+
return ts.sys.readDirectory(root, TS_EXTENSIONS, excludes, ["**/*"]).map((file) => resolve(file)).sort();
|
|
60
|
+
}
|
|
61
|
+
const configured = new Set();
|
|
62
|
+
collectTsconfigFiles(configPath, configured, new Set());
|
|
63
|
+
const allowed = new Set(ts.sys.readDirectory(root, TS_EXTENSIONS, excludes, ["**/*"]).map((file) => resolve(file)));
|
|
64
|
+
return [...configured].filter((file) => allowed.has(file)).sort();
|
|
23
65
|
}
|
|
24
66
|
function classifyCallee(callee, scope) {
|
|
25
67
|
if (ts.isIdentifier(callee)) {
|
|
@@ -45,7 +87,7 @@ function classifyCallee(callee, scope) {
|
|
|
45
87
|
return { kind: "transaction" };
|
|
46
88
|
if (methodName === "file")
|
|
47
89
|
return { kind: "file" };
|
|
48
|
-
if (methodName === "one" || methodName === "optional")
|
|
90
|
+
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
49
91
|
return { kind: "inline" };
|
|
50
92
|
return null;
|
|
51
93
|
}
|
|
@@ -57,7 +99,7 @@ function classifyCallee(callee, scope) {
|
|
|
57
99
|
if (ts.isIdentifier(mid.expression) && scope.namespaces.has(mid.expression.text)) {
|
|
58
100
|
if (mid.name.text !== "sql")
|
|
59
101
|
return null;
|
|
60
|
-
if (methodName === "one" || methodName === "optional")
|
|
102
|
+
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
61
103
|
return { kind: "inline" };
|
|
62
104
|
if (methodName === "file")
|
|
63
105
|
return { kind: "file" };
|
|
@@ -72,7 +114,7 @@ function classifyCallee(callee, scope) {
|
|
|
72
114
|
return null;
|
|
73
115
|
if (mid.name.text !== "file")
|
|
74
116
|
return null;
|
|
75
|
-
if (methodName === "one" || methodName === "optional")
|
|
117
|
+
if (methodName === "one" || methodName === "optional" || methodName === "execute")
|
|
76
118
|
return { kind: "file" };
|
|
77
119
|
return null;
|
|
78
120
|
}
|
|
@@ -83,13 +125,13 @@ function classifyCallee(callee, scope) {
|
|
|
83
125
|
scope.namespaces.has(mid.expression.expression.text) &&
|
|
84
126
|
mid.expression.name.text === "sql" &&
|
|
85
127
|
mid.name.text === "file" &&
|
|
86
|
-
(methodName === "one" || methodName === "optional")) {
|
|
128
|
+
(methodName === "one" || methodName === "optional" || methodName === "execute")) {
|
|
87
129
|
return { kind: "file" };
|
|
88
130
|
}
|
|
89
131
|
}
|
|
90
132
|
return null;
|
|
91
133
|
}
|
|
92
|
-
export function scanFile(absPath, root) {
|
|
134
|
+
export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
|
|
93
135
|
const text = readFileSync(absPath, "utf8");
|
|
94
136
|
const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TSX);
|
|
95
137
|
const importedAliases = new Set();
|
|
@@ -100,7 +142,7 @@ export function scanFile(absPath, root) {
|
|
|
100
142
|
const mod = stmt.moduleSpecifier;
|
|
101
143
|
if (!ts.isStringLiteral(mod))
|
|
102
144
|
continue;
|
|
103
|
-
if (!
|
|
145
|
+
if (!modules.includes(mod.text))
|
|
104
146
|
continue;
|
|
105
147
|
const ic = stmt.importClause;
|
|
106
148
|
if (!ic)
|
|
@@ -130,7 +172,7 @@ export function scanFile(absPath, root) {
|
|
|
130
172
|
const recordInline = (first, args) => {
|
|
131
173
|
if (!ts.isStringLiteralLike(first)) {
|
|
132
174
|
const pos = here(first);
|
|
133
|
-
throw new
|
|
175
|
+
throw new ScanError(fileRel, pos.line, pos.column, "sql() requires a string literal as first argument");
|
|
134
176
|
}
|
|
135
177
|
const pos = here(first);
|
|
136
178
|
out.push({
|
|
@@ -146,13 +188,22 @@ export function scanFile(absPath, root) {
|
|
|
146
188
|
const recordFile = (first, args, callee) => {
|
|
147
189
|
if (!ts.isStringLiteralLike(first)) {
|
|
148
190
|
const pos = first ? here(first) : here(callee);
|
|
149
|
-
throw new
|
|
191
|
+
throw new ScanError(fileRel, pos.line, pos.column, "sql.file() requires a string literal path");
|
|
150
192
|
}
|
|
151
193
|
const sqlPath = first.text;
|
|
152
|
-
|
|
194
|
+
if (isAbsolute(sqlPath)) {
|
|
195
|
+
const pos = here(first);
|
|
196
|
+
throw new ScanError(fileRel, pos.line, pos.column, `sql.file path must be relative to --root: ${sqlPath}`);
|
|
197
|
+
}
|
|
198
|
+
const abs = resolve(root, sqlPath);
|
|
199
|
+
const rel = relative(root, abs);
|
|
200
|
+
if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
|
|
201
|
+
const pos = here(first);
|
|
202
|
+
throw new ScanError(fileRel, pos.line, pos.column, `sql.file path escapes --root: ${sqlPath}`);
|
|
203
|
+
}
|
|
153
204
|
if (!existsSync(abs)) {
|
|
154
205
|
const pos = here(first);
|
|
155
|
-
throw new
|
|
206
|
+
throw new ScanError(fileRel, pos.line, pos.column, `sql.file path not found: ${sqlPath}`);
|
|
156
207
|
}
|
|
157
208
|
const query = readFileSync(abs, "utf8");
|
|
158
209
|
const pos = here(first);
|
|
@@ -163,7 +214,7 @@ export function scanFile(absPath, root) {
|
|
|
163
214
|
query,
|
|
164
215
|
paramCount: args.length - 1,
|
|
165
216
|
kind: "file",
|
|
166
|
-
sqlFilePath:
|
|
217
|
+
sqlFilePath: sqlPath,
|
|
167
218
|
});
|
|
168
219
|
return true;
|
|
169
220
|
};
|
|
@@ -272,11 +323,11 @@ export function scanFile(absPath, root) {
|
|
|
272
323
|
visit(source, { sqlAliases: importedAliases, namespaces: importedNamespaces });
|
|
273
324
|
return out;
|
|
274
325
|
}
|
|
275
|
-
export function scanProject(root) {
|
|
276
|
-
const files = findSourceFiles(root);
|
|
326
|
+
export function scanProject(root, scan = {}) {
|
|
327
|
+
const files = findSourceFiles(root, scan);
|
|
277
328
|
const out = [];
|
|
278
329
|
for (const f of files) {
|
|
279
|
-
for (const site of scanFile(f, root))
|
|
330
|
+
for (const site of scanFile(f, root, scan.modules ?? DEFAULT_SQLX_MODULES))
|
|
280
331
|
out.push(site);
|
|
281
332
|
}
|
|
282
333
|
return out;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function containsUnknownType(type: string): boolean;
|