@onreza/sqlx-js 0.0.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/LICENSE +21 -0
- package/README.md +496 -0
- package/ROADMAP.md +28 -0
- package/dist/bin/sqlx-js.d.ts +2 -0
- package/dist/bin/sqlx-js.js +180 -0
- package/dist/src/bun-runtime.d.ts +7 -0
- package/dist/src/bun-runtime.js +45 -0
- package/dist/src/bun.d.ts +22 -0
- package/dist/src/bun.js +9 -0
- package/dist/src/cache.d.ts +36 -0
- package/dist/src/cache.js +104 -0
- package/dist/src/codegen.d.ts +2 -0
- package/dist/src/codegen.js +74 -0
- package/dist/src/commands/migrate.d.ts +63 -0
- package/dist/src/commands/migrate.js +283 -0
- package/dist/src/commands/prepare.d.ts +23 -0
- package/dist/src/commands/prepare.js +314 -0
- package/dist/src/commands/schema.d.ts +14 -0
- package/dist/src/commands/schema.js +72 -0
- package/dist/src/commands/watch.d.ts +21 -0
- package/dist/src/commands/watch.js +94 -0
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.js +23 -0
- package/dist/src/index.d.ts +23 -0
- package/dist/src/index.js +10 -0
- package/dist/src/pg/analyze.d.ts +13 -0
- package/dist/src/pg/analyze.js +479 -0
- package/dist/src/pg/extensions.d.ts +2 -0
- package/dist/src/pg/extensions.js +15 -0
- package/dist/src/pg/narrow.d.ts +3 -0
- package/dist/src/pg/narrow.js +146 -0
- package/dist/src/pg/oids.d.ts +10 -0
- package/dist/src/pg/oids.js +137 -0
- package/dist/src/pg/param-map.d.ts +12 -0
- package/dist/src/pg/param-map.js +180 -0
- package/dist/src/pg/schema.d.ts +61 -0
- package/dist/src/pg/schema.js +225 -0
- package/dist/src/pg/wire.d.ts +134 -0
- package/dist/src/pg/wire.js +705 -0
- package/dist/src/postgres-runtime.d.ts +11 -0
- package/dist/src/postgres-runtime.js +150 -0
- package/dist/src/runtime.d.ts +69 -0
- package/dist/src/runtime.js +446 -0
- package/dist/src/scan/scanner.d.ts +12 -0
- package/dist/src/scan/scanner.js +283 -0
- package/dist/src/schema-snapshot.d.ts +104 -0
- package/dist/src/schema-snapshot.js +473 -0
- package/dist/src/typed.d.ts +25 -0
- package/dist/src/typed.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
const EXCLUDE_DIRS = new Set(["node_modules", ".git", ".sqlx-js", "dist", "build", ".next"]);
|
|
5
|
+
const SQLX_MODULES = new Set(["@onreza/sqlx-js", "@onreza/sqlx-js/bun"]);
|
|
6
|
+
const EXT = /\.(ts|tsx|mts|cts)$/;
|
|
7
|
+
function walk(dir, out) {
|
|
8
|
+
for (const name of readdirSync(dir)) {
|
|
9
|
+
if (EXCLUDE_DIRS.has(name))
|
|
10
|
+
continue;
|
|
11
|
+
const full = join(dir, name);
|
|
12
|
+
const st = statSync(full);
|
|
13
|
+
if (st.isDirectory())
|
|
14
|
+
walk(full, out);
|
|
15
|
+
else if (EXT.test(name))
|
|
16
|
+
out.push(full);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function findSourceFiles(root) {
|
|
20
|
+
const out = [];
|
|
21
|
+
walk(root, out);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function classifyCallee(callee, scope) {
|
|
25
|
+
if (ts.isIdentifier(callee)) {
|
|
26
|
+
if (!scope.sqlAliases.has(callee.text))
|
|
27
|
+
return null;
|
|
28
|
+
return { kind: "inline" };
|
|
29
|
+
}
|
|
30
|
+
if (!ts.isPropertyAccessExpression(callee))
|
|
31
|
+
return null;
|
|
32
|
+
if (!ts.isIdentifier(callee.name))
|
|
33
|
+
return null;
|
|
34
|
+
const methodName = callee.name.text;
|
|
35
|
+
if (ts.isIdentifier(callee.expression)) {
|
|
36
|
+
const id = callee.expression.text;
|
|
37
|
+
if (scope.namespaces.has(id)) {
|
|
38
|
+
if (methodName === "sql")
|
|
39
|
+
return { kind: "inline" };
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!scope.sqlAliases.has(id))
|
|
43
|
+
return null;
|
|
44
|
+
if (methodName === "transaction")
|
|
45
|
+
return { kind: "transaction" };
|
|
46
|
+
if (methodName === "file")
|
|
47
|
+
return { kind: "file" };
|
|
48
|
+
if (methodName === "one" || methodName === "optional")
|
|
49
|
+
return { kind: "inline" };
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (ts.isPropertyAccessExpression(callee.expression)) {
|
|
53
|
+
const mid = callee.expression;
|
|
54
|
+
if (!ts.isIdentifier(mid.name))
|
|
55
|
+
return null;
|
|
56
|
+
// ns.sql.X(...) chains
|
|
57
|
+
if (ts.isIdentifier(mid.expression) && scope.namespaces.has(mid.expression.text)) {
|
|
58
|
+
if (mid.name.text !== "sql")
|
|
59
|
+
return null;
|
|
60
|
+
if (methodName === "one" || methodName === "optional")
|
|
61
|
+
return { kind: "inline" };
|
|
62
|
+
if (methodName === "file")
|
|
63
|
+
return { kind: "file" };
|
|
64
|
+
if (methodName === "transaction")
|
|
65
|
+
return { kind: "transaction" };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
// sqlAlias.file.X(...) — file.one / file.optional
|
|
69
|
+
if (ts.isIdentifier(mid.expression)) {
|
|
70
|
+
const root = mid.expression.text;
|
|
71
|
+
if (!scope.sqlAliases.has(root))
|
|
72
|
+
return null;
|
|
73
|
+
if (mid.name.text !== "file")
|
|
74
|
+
return null;
|
|
75
|
+
if (methodName === "one" || methodName === "optional")
|
|
76
|
+
return { kind: "file" };
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// ns.sql.file.X(...) chains
|
|
80
|
+
if (ts.isPropertyAccessExpression(mid.expression) &&
|
|
81
|
+
ts.isIdentifier(mid.expression.expression) &&
|
|
82
|
+
ts.isIdentifier(mid.expression.name) &&
|
|
83
|
+
scope.namespaces.has(mid.expression.expression.text) &&
|
|
84
|
+
mid.expression.name.text === "sql" &&
|
|
85
|
+
mid.name.text === "file" &&
|
|
86
|
+
(methodName === "one" || methodName === "optional")) {
|
|
87
|
+
return { kind: "file" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
export function scanFile(absPath, root) {
|
|
93
|
+
const text = readFileSync(absPath, "utf8");
|
|
94
|
+
const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TSX);
|
|
95
|
+
const importedAliases = new Set();
|
|
96
|
+
const importedNamespaces = new Set();
|
|
97
|
+
for (const stmt of source.statements) {
|
|
98
|
+
if (!ts.isImportDeclaration(stmt))
|
|
99
|
+
continue;
|
|
100
|
+
const mod = stmt.moduleSpecifier;
|
|
101
|
+
if (!ts.isStringLiteral(mod))
|
|
102
|
+
continue;
|
|
103
|
+
if (!SQLX_MODULES.has(mod.text))
|
|
104
|
+
continue;
|
|
105
|
+
const ic = stmt.importClause;
|
|
106
|
+
if (!ic)
|
|
107
|
+
continue;
|
|
108
|
+
const nb = ic.namedBindings;
|
|
109
|
+
if (!nb)
|
|
110
|
+
continue;
|
|
111
|
+
if (ts.isNamespaceImport(nb)) {
|
|
112
|
+
importedNamespaces.add(nb.name.text);
|
|
113
|
+
}
|
|
114
|
+
else if (ts.isNamedImports(nb)) {
|
|
115
|
+
for (const elem of nb.elements) {
|
|
116
|
+
const orig = (elem.propertyName ?? elem.name).text;
|
|
117
|
+
if (orig === "sql")
|
|
118
|
+
importedAliases.add(elem.name.text);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (importedAliases.size === 0 && importedNamespaces.size === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const out = [];
|
|
125
|
+
const here = (node) => {
|
|
126
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
127
|
+
return { line: line + 1, column: character + 1 };
|
|
128
|
+
};
|
|
129
|
+
const fileRel = relative(root, absPath);
|
|
130
|
+
const recordInline = (first, args) => {
|
|
131
|
+
if (!ts.isStringLiteralLike(first)) {
|
|
132
|
+
const pos = here(first);
|
|
133
|
+
throw new Error(`sqlx-js: ${fileRel}:${pos.line}:${pos.column} — sql() requires a string literal as first argument`);
|
|
134
|
+
}
|
|
135
|
+
const pos = here(first);
|
|
136
|
+
out.push({
|
|
137
|
+
file: fileRel,
|
|
138
|
+
line: pos.line,
|
|
139
|
+
column: pos.column,
|
|
140
|
+
query: first.text,
|
|
141
|
+
paramCount: args.length - 1,
|
|
142
|
+
kind: "inline",
|
|
143
|
+
});
|
|
144
|
+
return true;
|
|
145
|
+
};
|
|
146
|
+
const recordFile = (first, args, callee) => {
|
|
147
|
+
if (!ts.isStringLiteralLike(first)) {
|
|
148
|
+
const pos = first ? here(first) : here(callee);
|
|
149
|
+
throw new Error(`sqlx-js: ${fileRel}:${pos.line}:${pos.column} — sql.file() requires a string literal path`);
|
|
150
|
+
}
|
|
151
|
+
const sqlPath = first.text;
|
|
152
|
+
const abs = resolve(dirname(absPath), sqlPath);
|
|
153
|
+
if (!existsSync(abs)) {
|
|
154
|
+
const pos = here(first);
|
|
155
|
+
throw new Error(`sqlx-js: ${fileRel}:${pos.line}:${pos.column} — sql.file path not found: ${sqlPath}`);
|
|
156
|
+
}
|
|
157
|
+
const query = readFileSync(abs, "utf8");
|
|
158
|
+
const pos = here(first);
|
|
159
|
+
out.push({
|
|
160
|
+
file: fileRel,
|
|
161
|
+
line: pos.line,
|
|
162
|
+
column: pos.column,
|
|
163
|
+
query,
|
|
164
|
+
paramCount: args.length - 1,
|
|
165
|
+
kind: "file",
|
|
166
|
+
sqlFilePath: relative(root, abs),
|
|
167
|
+
});
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
const bindingDeclares = (binding, name) => {
|
|
171
|
+
if (ts.isIdentifier(binding))
|
|
172
|
+
return binding.text === name;
|
|
173
|
+
for (const el of binding.elements) {
|
|
174
|
+
if (ts.isOmittedExpression(el))
|
|
175
|
+
continue;
|
|
176
|
+
if (bindingDeclares(el.name, name))
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
const scopeWithoutBindingShadows = (scope, bindings) => {
|
|
182
|
+
let changed = false;
|
|
183
|
+
const nextSql = new Set(scope.sqlAliases);
|
|
184
|
+
const nextNs = new Set(scope.namespaces);
|
|
185
|
+
for (const binding of bindings) {
|
|
186
|
+
for (const a of scope.sqlAliases) {
|
|
187
|
+
if (bindingDeclares(binding, a)) {
|
|
188
|
+
nextSql.delete(a);
|
|
189
|
+
changed = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const a of scope.namespaces) {
|
|
193
|
+
if (bindingDeclares(binding, a)) {
|
|
194
|
+
nextNs.delete(a);
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return changed ? { sqlAliases: nextSql, namespaces: nextNs } : scope;
|
|
200
|
+
};
|
|
201
|
+
const visit = (node, scope) => {
|
|
202
|
+
if (ts.isCallExpression(node)) {
|
|
203
|
+
const classified = classifyCallee(node.expression, scope);
|
|
204
|
+
if (classified) {
|
|
205
|
+
if (classified.kind === "transaction") {
|
|
206
|
+
const fn = node.arguments[node.arguments.length - 1];
|
|
207
|
+
if (fn && (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) {
|
|
208
|
+
const param = fn.parameters[0];
|
|
209
|
+
const shadowed = param ? scopeWithoutBindingShadows(scope, [param.name]) : scope;
|
|
210
|
+
const innerSql = new Set(shadowed.sqlAliases);
|
|
211
|
+
if (param && ts.isIdentifier(param.name)) {
|
|
212
|
+
innerSql.add(param.name.text);
|
|
213
|
+
}
|
|
214
|
+
visit(fn.body, { sqlAliases: innerSql, namespaces: shadowed.namespaces });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (classified.kind === "file") {
|
|
219
|
+
const first = node.arguments[0];
|
|
220
|
+
if (first)
|
|
221
|
+
recordFile(first, node.arguments, node.expression);
|
|
222
|
+
}
|
|
223
|
+
else if (classified.kind === "inline") {
|
|
224
|
+
const first = node.arguments[0];
|
|
225
|
+
if (first)
|
|
226
|
+
recordInline(first, node.arguments);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (ts.isBlock(node) || ts.isSourceFile(node) || ts.isModuleBlock(node)) {
|
|
231
|
+
let current = scope;
|
|
232
|
+
const stmts = node.statements;
|
|
233
|
+
for (const stmt of stmts) {
|
|
234
|
+
if (ts.isVariableStatement(stmt)) {
|
|
235
|
+
current = scopeWithoutBindingShadows(current, stmt.declarationList.declarations.map((d) => d.name));
|
|
236
|
+
}
|
|
237
|
+
else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
238
|
+
current = scopeWithoutBindingShadows(current, [stmt.name]);
|
|
239
|
+
}
|
|
240
|
+
visit(stmt, current);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (ts.isCatchClause(node) && node.variableDeclaration?.name) {
|
|
245
|
+
const next = scopeWithoutBindingShadows(scope, [node.variableDeclaration.name]);
|
|
246
|
+
if (next !== scope) {
|
|
247
|
+
visit(node.block, next);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if ((ts.isFunctionDeclaration(node) ||
|
|
252
|
+
ts.isFunctionExpression(node) ||
|
|
253
|
+
ts.isArrowFunction(node) ||
|
|
254
|
+
ts.isMethodDeclaration(node) ||
|
|
255
|
+
ts.isConstructorDeclaration(node) ||
|
|
256
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
257
|
+
ts.isSetAccessorDeclaration(node)) &&
|
|
258
|
+
node.body) {
|
|
259
|
+
const bindings = node.parameters.map((p) => p.name);
|
|
260
|
+
if (ts.isFunctionExpression(node) && node.name)
|
|
261
|
+
bindings.push(node.name);
|
|
262
|
+
const next = scopeWithoutBindingShadows(scope, bindings);
|
|
263
|
+
for (const param of node.parameters) {
|
|
264
|
+
if (param.initializer)
|
|
265
|
+
visit(param.initializer, next);
|
|
266
|
+
}
|
|
267
|
+
visit(node.body, next);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
ts.forEachChild(node, (child) => visit(child, scope));
|
|
271
|
+
};
|
|
272
|
+
visit(source, { sqlAliases: importedAliases, namespaces: importedNamespaces });
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
export function scanProject(root) {
|
|
276
|
+
const files = findSourceFiles(root);
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const f of files) {
|
|
279
|
+
for (const site of scanFile(f, root))
|
|
280
|
+
out.push(site);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PgClient } from "./pg/wire.js";
|
|
2
|
+
export type SchemaRelationKind = "table" | "partitioned_table" | "view" | "materialized_view" | "foreign_table";
|
|
3
|
+
export type SchemaConstraintKind = "primary_key" | "foreign_key" | "unique" | "check" | "exclude";
|
|
4
|
+
export type SchemaFunctionKind = "function" | "procedure" | "aggregate" | "window";
|
|
5
|
+
export type SchemaColumnSnapshot = {
|
|
6
|
+
name: string;
|
|
7
|
+
ordinal: number;
|
|
8
|
+
type: string;
|
|
9
|
+
typeOid: number;
|
|
10
|
+
nullable: boolean;
|
|
11
|
+
writable: boolean;
|
|
12
|
+
default?: string;
|
|
13
|
+
identity?: "always" | "by_default";
|
|
14
|
+
generated?: "stored";
|
|
15
|
+
generatedExpression?: string;
|
|
16
|
+
};
|
|
17
|
+
export type SchemaConstraintSnapshot = {
|
|
18
|
+
name: string;
|
|
19
|
+
kind: SchemaConstraintKind;
|
|
20
|
+
columns: string[];
|
|
21
|
+
definition: string;
|
|
22
|
+
expression?: string;
|
|
23
|
+
references?: {
|
|
24
|
+
schema: string;
|
|
25
|
+
table: string;
|
|
26
|
+
columns: string[];
|
|
27
|
+
onUpdate: string;
|
|
28
|
+
onDelete: string;
|
|
29
|
+
};
|
|
30
|
+
deferrable?: boolean;
|
|
31
|
+
initiallyDeferred?: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type SchemaIndexSnapshot = {
|
|
34
|
+
name: string;
|
|
35
|
+
unique: boolean;
|
|
36
|
+
primary: boolean;
|
|
37
|
+
method: string;
|
|
38
|
+
columns: string[];
|
|
39
|
+
definition: string;
|
|
40
|
+
predicate?: string;
|
|
41
|
+
};
|
|
42
|
+
export type SchemaRelationSnapshot = {
|
|
43
|
+
schema: string;
|
|
44
|
+
name: string;
|
|
45
|
+
kind: SchemaRelationKind;
|
|
46
|
+
columns: SchemaColumnSnapshot[];
|
|
47
|
+
constraints: SchemaConstraintSnapshot[];
|
|
48
|
+
indexes: SchemaIndexSnapshot[];
|
|
49
|
+
definition?: string;
|
|
50
|
+
};
|
|
51
|
+
export type SchemaEnumSnapshot = {
|
|
52
|
+
kind: "enum";
|
|
53
|
+
schema: string;
|
|
54
|
+
name: string;
|
|
55
|
+
values: string[];
|
|
56
|
+
};
|
|
57
|
+
export type SchemaDomainSnapshot = {
|
|
58
|
+
kind: "domain";
|
|
59
|
+
schema: string;
|
|
60
|
+
name: string;
|
|
61
|
+
baseType: string;
|
|
62
|
+
notNull: boolean;
|
|
63
|
+
default?: string;
|
|
64
|
+
checks: string[];
|
|
65
|
+
};
|
|
66
|
+
export type SchemaCompositeSnapshot = {
|
|
67
|
+
kind: "composite";
|
|
68
|
+
schema: string;
|
|
69
|
+
name: string;
|
|
70
|
+
fields: {
|
|
71
|
+
name: string;
|
|
72
|
+
type: string;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
export type SchemaTypeSnapshot = SchemaEnumSnapshot | SchemaDomainSnapshot | SchemaCompositeSnapshot;
|
|
76
|
+
export type SchemaFunctionSnapshot = {
|
|
77
|
+
schema: string;
|
|
78
|
+
name: string;
|
|
79
|
+
kind: SchemaFunctionKind;
|
|
80
|
+
identityArguments: string;
|
|
81
|
+
arguments: string;
|
|
82
|
+
returnType: string;
|
|
83
|
+
returnsSet: boolean;
|
|
84
|
+
volatility: "immutable" | "stable" | "volatile";
|
|
85
|
+
strict: boolean;
|
|
86
|
+
securityDefiner: boolean;
|
|
87
|
+
language: string;
|
|
88
|
+
};
|
|
89
|
+
export type SchemaSnapshot = {
|
|
90
|
+
version: 1;
|
|
91
|
+
schemas: string[];
|
|
92
|
+
relations: SchemaRelationSnapshot[];
|
|
93
|
+
types: SchemaTypeSnapshot[];
|
|
94
|
+
functions: SchemaFunctionSnapshot[];
|
|
95
|
+
};
|
|
96
|
+
export declare function introspectDatabase(databaseUrl: string): Promise<SchemaSnapshot>;
|
|
97
|
+
export declare function introspectConnected(client: PgClient): Promise<SchemaSnapshot>;
|
|
98
|
+
export declare function stableSchemaJson(snapshot: SchemaSnapshot): string;
|
|
99
|
+
export declare function writeSchemaSnapshot(path: string, snapshot: SchemaSnapshot): void;
|
|
100
|
+
export declare function readSchemaSnapshot(path: string): SchemaSnapshot;
|
|
101
|
+
export declare function schemaSnapshotEqual(a: SchemaSnapshot, b: SchemaSnapshot): boolean;
|
|
102
|
+
export declare function renderSchemaManifest(snapshot: SchemaSnapshot): string;
|
|
103
|
+
export declare function writeSchemaManifest(path: string, snapshot: SchemaSnapshot): void;
|
|
104
|
+
export declare function schemaSnapshotExists(path: string): boolean;
|