@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/cache.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { isBuiltinOid } from "./pg/oids.js";
|
|
5
|
+
export const CACHE_FORMAT_VERSION = 2;
|
|
6
|
+
export const GENERATOR_REVISION = 3;
|
|
7
|
+
export const CACHE_MANIFEST_FILE = "cache-manifest.json";
|
|
8
|
+
export function portableCacheOid(oid) {
|
|
9
|
+
return isBuiltinOid(oid) ? oid : 0;
|
|
10
|
+
}
|
|
4
11
|
export function fingerprint(query) {
|
|
5
12
|
const norm = normalizeForFingerprint(query);
|
|
6
13
|
return createHash("sha256").update(norm).digest("hex").slice(0, 16);
|
|
@@ -202,11 +209,35 @@ export class Cache {
|
|
|
202
209
|
throw err;
|
|
203
210
|
}
|
|
204
211
|
}
|
|
212
|
+
replaceAll(entries, prune = true) {
|
|
213
|
+
this.ensure();
|
|
214
|
+
const staged = [];
|
|
215
|
+
try {
|
|
216
|
+
for (const { fp, entry } of entries) {
|
|
217
|
+
const final = join(this.dir, `${fp}.json`);
|
|
218
|
+
const tmp = `${final}.tmp-${randomBytes(4).toString("hex")}`;
|
|
219
|
+
writeFileSync(tmp, JSON.stringify(entry, null, 2));
|
|
220
|
+
staged.push({ fp, tmp, final });
|
|
221
|
+
}
|
|
222
|
+
for (const item of staged)
|
|
223
|
+
renameSync(item.tmp, item.final);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
for (const item of staged) {
|
|
227
|
+
try {
|
|
228
|
+
unlinkSync(item.tmp);
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
return prune ? this.prune(staged.map((item) => item.fp)) : [];
|
|
235
|
+
}
|
|
205
236
|
list() {
|
|
206
237
|
if (!existsSync(this.dir))
|
|
207
238
|
return [];
|
|
208
239
|
return readdirSync(this.dir)
|
|
209
|
-
.filter((f) => f.endsWith(".json") && !f.includes(".tmp-"))
|
|
240
|
+
.filter((f) => f !== CACHE_MANIFEST_FILE && f.endsWith(".json") && !f.includes(".tmp-"))
|
|
210
241
|
.map((f) => {
|
|
211
242
|
const fp = f.replace(/\.json$/, "");
|
|
212
243
|
return { fp, entry: assertEntryShape(fp, parseEntryJson(join(this.dir, f))) };
|
|
@@ -229,3 +260,54 @@ export class Cache {
|
|
|
229
260
|
return removed;
|
|
230
261
|
}
|
|
231
262
|
}
|
|
263
|
+
export function cacheManifestPath(cacheDir) {
|
|
264
|
+
return join(cacheDir, CACHE_MANIFEST_FILE);
|
|
265
|
+
}
|
|
266
|
+
export function writeCacheManifest(cacheDir, configHash) {
|
|
267
|
+
if (!existsSync(cacheDir))
|
|
268
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
269
|
+
const path = cacheManifestPath(cacheDir);
|
|
270
|
+
const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
|
|
271
|
+
const manifest = {
|
|
272
|
+
cacheFormat: CACHE_FORMAT_VERSION,
|
|
273
|
+
generatorRevision: GENERATOR_REVISION,
|
|
274
|
+
configHash,
|
|
275
|
+
};
|
|
276
|
+
writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
|
|
277
|
+
try {
|
|
278
|
+
renameSync(tmp, path);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
try {
|
|
282
|
+
unlinkSync(tmp);
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export function readCacheManifest(cacheDir) {
|
|
289
|
+
const path = cacheManifestPath(cacheDir);
|
|
290
|
+
if (!existsSync(path))
|
|
291
|
+
return null;
|
|
292
|
+
const raw = parseEntryJson(path);
|
|
293
|
+
if (!raw || typeof raw !== "object") {
|
|
294
|
+
throw new Error(`sqlx-js: cache manifest is malformed: ${path}`);
|
|
295
|
+
}
|
|
296
|
+
const value = raw;
|
|
297
|
+
if (value.cacheFormat !== CACHE_FORMAT_VERSION ||
|
|
298
|
+
value.generatorRevision !== GENERATOR_REVISION ||
|
|
299
|
+
typeof value.configHash !== "string") {
|
|
300
|
+
throw new Error(`sqlx-js: cache manifest is stale: ${path}. Run \`sqlx-js prepare\`.`);
|
|
301
|
+
}
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
export function assertCacheManifest(cacheDir, configHash) {
|
|
305
|
+
const manifest = readCacheManifest(cacheDir);
|
|
306
|
+
if (!manifest) {
|
|
307
|
+
throw new Error(`sqlx-js: cache manifest is missing. Run \`sqlx-js prepare\` to regenerate the cache.`);
|
|
308
|
+
}
|
|
309
|
+
if (manifest.configHash !== configHash) {
|
|
310
|
+
throw new Error("sqlx-js: cache was generated with a different jsonbTypes/customTypes config. Run `sqlx-js prepare`.");
|
|
311
|
+
}
|
|
312
|
+
return manifest;
|
|
313
|
+
}
|
package/dist/src/codegen.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { dirname } from "node:path";
|
|
3
4
|
import { effectiveNullable } from "./cache.js";
|
|
4
5
|
function entrySignature(e) {
|
|
@@ -25,7 +26,18 @@ export function emitDts(outPath, entries, functions = []) {
|
|
|
25
26
|
emitModule(lines, "@onreza/sqlx-js", entries, functions);
|
|
26
27
|
lines.push("");
|
|
27
28
|
lines.push("export {};");
|
|
28
|
-
|
|
29
|
+
const tmp = `${outPath}.tmp-${randomBytes(4).toString("hex")}`;
|
|
30
|
+
writeFileSync(tmp, lines.join("\n") + "\n");
|
|
31
|
+
try {
|
|
32
|
+
renameSync(tmp, outPath);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
try {
|
|
36
|
+
unlinkSync(tmp);
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
function emitModule(lines, moduleName, entries, functions) {
|
|
31
43
|
lines.push(`declare module ${JSON.stringify(moduleName)} {`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DoctorCheck = {
|
|
2
|
+
name: string;
|
|
3
|
+
status: "ok" | "warning" | "error";
|
|
4
|
+
message: string;
|
|
5
|
+
details?: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export type DoctorOptions = {
|
|
8
|
+
root: string;
|
|
9
|
+
databaseUrl: string;
|
|
10
|
+
cacheDir: string;
|
|
11
|
+
dtsPath: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
envError?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function inspectDoctor(opts: DoctorOptions): Promise<DoctorCheck[]>;
|
|
16
|
+
export declare function runDoctor(opts: DoctorOptions): Promise<void>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { assertCacheManifest } from "../cache.js";
|
|
5
|
+
import { assertSupportedRuntime, envFilePath, loadConfigInfo, nativeTypeScriptEnabled, prepareConfigHash, runtimeVersion, } from "../config.js";
|
|
6
|
+
import { decodeText, parseDatabaseUrl, PgClient } from "../pg/wire.js";
|
|
7
|
+
import { probePgschema } from "./pgschema.js";
|
|
8
|
+
function decodeBoolean(value) {
|
|
9
|
+
const text = decodeText(value ?? null);
|
|
10
|
+
return text === "t" || text === "true" || text === "1";
|
|
11
|
+
}
|
|
12
|
+
function tsconfigIncludes(root, file) {
|
|
13
|
+
const path = join(root, "tsconfig.json");
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return { ok: false, message: `tsconfig.json not found at ${path}` };
|
|
16
|
+
const target = resolve(file);
|
|
17
|
+
const visited = new Set();
|
|
18
|
+
const visit = (configPath) => {
|
|
19
|
+
const resolvedConfig = resolve(configPath);
|
|
20
|
+
if (visited.has(resolvedConfig))
|
|
21
|
+
return { included: false };
|
|
22
|
+
visited.add(resolvedConfig);
|
|
23
|
+
const read = ts.readConfigFile(resolvedConfig, ts.sys.readFile);
|
|
24
|
+
if (read.error) {
|
|
25
|
+
return { included: false, error: ts.flattenDiagnosticMessageText(read.error.messageText, "\n") };
|
|
26
|
+
}
|
|
27
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, resolve(resolvedConfig, ".."), undefined, resolvedConfig);
|
|
28
|
+
if (parsed.errors.length > 0) {
|
|
29
|
+
return {
|
|
30
|
+
included: false,
|
|
31
|
+
error: parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("; "),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (parsed.fileNames.some((name) => resolve(name) === target))
|
|
35
|
+
return { included: true };
|
|
36
|
+
for (const reference of parsed.projectReferences ?? []) {
|
|
37
|
+
const result = visit(ts.resolveProjectReferencePath(reference));
|
|
38
|
+
if (result.included || result.error)
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
return { included: false };
|
|
42
|
+
};
|
|
43
|
+
const result = visit(path);
|
|
44
|
+
if (result.error)
|
|
45
|
+
return { ok: false, message: result.error };
|
|
46
|
+
const included = result.included;
|
|
47
|
+
return included
|
|
48
|
+
? { ok: true, message: `${file} is included by tsconfig.json` }
|
|
49
|
+
: { ok: false, message: `${file} is not included by tsconfig.json` };
|
|
50
|
+
}
|
|
51
|
+
export async function inspectDoctor(opts) {
|
|
52
|
+
const checks = [];
|
|
53
|
+
const runtime = runtimeVersion();
|
|
54
|
+
try {
|
|
55
|
+
assertSupportedRuntime();
|
|
56
|
+
checks.push({
|
|
57
|
+
name: "runtime",
|
|
58
|
+
status: "ok",
|
|
59
|
+
message: `${runtime.runtime} ${runtime.version} satisfies the supported baseline`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
checks.push({ name: "runtime", status: "error", message: e.message });
|
|
64
|
+
}
|
|
65
|
+
let config = {};
|
|
66
|
+
let configLoaded = true;
|
|
67
|
+
try {
|
|
68
|
+
const info = await loadConfigInfo(opts.root);
|
|
69
|
+
config = info.config;
|
|
70
|
+
const nativeTs = nativeTypeScriptEnabled();
|
|
71
|
+
if (info.path && /\.m?ts$/.test(info.path) && nativeTs === false) {
|
|
72
|
+
checks.push({ name: "config", status: "error", message: "native TypeScript execution is disabled" });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
checks.push({
|
|
76
|
+
name: "config",
|
|
77
|
+
status: "ok",
|
|
78
|
+
message: info.path ? `loaded ${info.path}` : "no config file; defaults are active",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
configLoaded = false;
|
|
84
|
+
checks.push({ name: "config", status: "error", message: e.message });
|
|
85
|
+
}
|
|
86
|
+
const envPath = envFilePath(opts.root);
|
|
87
|
+
checks.push(opts.envError
|
|
88
|
+
? { name: "env", status: "error", message: opts.envError }
|
|
89
|
+
: existsSync(envPath) && !opts.databaseUrl
|
|
90
|
+
? { name: "env", status: "error", message: `${envPath} exists but DATABASE_URL is missing` }
|
|
91
|
+
: existsSync(envPath)
|
|
92
|
+
? { name: "env", status: "ok", message: `loaded environment file ${envPath}` }
|
|
93
|
+
: opts.databaseUrl
|
|
94
|
+
? { name: "env", status: "ok", message: "DATABASE_URL is provided by the process environment" }
|
|
95
|
+
: { name: "env", status: "error", message: `DATABASE_URL is missing and ${envPath} does not exist` });
|
|
96
|
+
if (!configLoaded) {
|
|
97
|
+
checks.push({ name: "cache", status: "warning", message: "cache config-hash check skipped because config failed to load" });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
try {
|
|
101
|
+
assertCacheManifest(opts.cacheDir, prepareConfigHash(config));
|
|
102
|
+
checks.push({ name: "cache", status: "ok", message: "cache manifest and config hash are current" });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
checks.push({ name: "cache", status: "error", message: e.message });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const tsconfig = tsconfigIncludes(opts.root, opts.dtsPath);
|
|
109
|
+
checks.push({
|
|
110
|
+
name: "tsconfig",
|
|
111
|
+
status: tsconfig.ok ? "ok" : "error",
|
|
112
|
+
message: tsconfig.message,
|
|
113
|
+
});
|
|
114
|
+
if (!opts.databaseUrl) {
|
|
115
|
+
checks.push({ name: "database", status: "error", message: "DATABASE_URL is required for the database check" });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const client = new PgClient(parseDatabaseUrl(opts.databaseUrl));
|
|
119
|
+
try {
|
|
120
|
+
await client.connect();
|
|
121
|
+
await client.describe("SELECT 1");
|
|
122
|
+
const result = await client.simpleQueryAll(`
|
|
123
|
+
SELECT current_setting('server_version'), current_user, current_database(),
|
|
124
|
+
(SELECT (rolcreatedb OR rolsuper)::text FROM pg_roles WHERE rolname = current_user),
|
|
125
|
+
COALESCE(has_schema_privilege(current_user, current_schema(), 'USAGE'), false)::text
|
|
126
|
+
`);
|
|
127
|
+
const row = result.rows[0];
|
|
128
|
+
const canCreateDatabase = decodeBoolean(row[3]);
|
|
129
|
+
const hasSchemaUsage = decodeBoolean(row[4]);
|
|
130
|
+
const shadowDatabaseUrl = process.env.SHADOW_DATABASE_URL;
|
|
131
|
+
const shadowAdminDatabaseUrl = process.env.SHADOW_ADMIN_DATABASE_URL;
|
|
132
|
+
const hasShadowFallback = Boolean(shadowDatabaseUrl || shadowAdminDatabaseUrl);
|
|
133
|
+
const needsShadowCreate = config.schema?.provider !== "pgschema";
|
|
134
|
+
checks.push({
|
|
135
|
+
name: "database",
|
|
136
|
+
status: "ok",
|
|
137
|
+
message: `connected to ${decodeText(row[2])} as ${decodeText(row[1])}`,
|
|
138
|
+
details: {
|
|
139
|
+
serverVersion: decodeText(row[0]),
|
|
140
|
+
describe: true,
|
|
141
|
+
schemaUsage: hasSchemaUsage,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
checks.push({
|
|
145
|
+
name: "permissions",
|
|
146
|
+
status: hasSchemaUsage && (!needsShadowCreate || canCreateDatabase || hasShadowFallback) ? "ok" : "warning",
|
|
147
|
+
message: !hasSchemaUsage
|
|
148
|
+
? "current user lacks USAGE on the current schema"
|
|
149
|
+
: !needsShadowCreate
|
|
150
|
+
? "current user can use the current schema; pgschema does not require shadow-database creation"
|
|
151
|
+
: canCreateDatabase
|
|
152
|
+
? "current user can use the current schema and create shadow databases"
|
|
153
|
+
: hasShadowFallback
|
|
154
|
+
? "current user cannot create databases; a shadow database fallback is configured"
|
|
155
|
+
: "current user cannot create shadow databases; configure SHADOW_ADMIN_DATABASE_URL or SHADOW_DATABASE_URL",
|
|
156
|
+
details: {
|
|
157
|
+
schemaUsage: hasSchemaUsage,
|
|
158
|
+
createDatabase: canCreateDatabase,
|
|
159
|
+
shadowDatabaseConfigured: Boolean(shadowDatabaseUrl),
|
|
160
|
+
shadowAdminConfigured: Boolean(shadowAdminDatabaseUrl),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
checks.push({ name: "database", status: "error", message: e.message });
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await client.end().catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!configLoaded) {
|
|
172
|
+
checks.push({ name: "pgschema", status: "warning", message: "schema-provider check skipped because config failed to load" });
|
|
173
|
+
}
|
|
174
|
+
else if (config.schema?.provider === "pgschema") {
|
|
175
|
+
const probe = probePgschema(opts.root, config);
|
|
176
|
+
checks.push({ name: "pgschema", status: probe.ok ? "ok" : "error", message: probe.message });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
checks.push({ name: "pgschema", status: "ok", message: "built-in migration provider is active" });
|
|
180
|
+
}
|
|
181
|
+
return checks;
|
|
182
|
+
}
|
|
183
|
+
export async function runDoctor(opts) {
|
|
184
|
+
const checks = await inspectDoctor(opts);
|
|
185
|
+
const ok = checks.every((check) => check.status !== "error");
|
|
186
|
+
if (opts.json) {
|
|
187
|
+
console.log(JSON.stringify({ formatVersion: 1, ok, checks }, null, 2));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
for (const check of checks) {
|
|
191
|
+
console.log(`${check.status.padEnd(7)} ${check.name}: ${check.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!ok)
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
const CONFIG_TEMPLATE = `import
|
|
3
|
+
const CONFIG_TEMPLATE = `import { defineConfig } from "@onreza/sqlx-js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export default defineConfig({
|
|
6
6
|
// Map jsonb columns/params to TypeScript types declared in a .d.ts, e.g.
|
|
7
7
|
// "users.settings": "SqlxJsJson.UserSettings",
|
|
8
8
|
jsonbTypes: {},
|
|
9
9
|
// Map PostgreSQL type names to TypeScript types, e.g.
|
|
10
10
|
// geometry: "GeoJSON.Geometry",
|
|
11
11
|
customTypes: {},
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export default config;
|
|
12
|
+
});
|
|
15
13
|
`;
|
|
16
|
-
const PGSCHEMA_CONFIG_TEMPLATE = `import
|
|
14
|
+
const PGSCHEMA_CONFIG_TEMPLATE = `import { defineConfig } from "@onreza/sqlx-js";
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
export default defineConfig({
|
|
19
17
|
schema: {
|
|
20
18
|
provider: "pgschema",
|
|
21
19
|
file: "schema.sql",
|
|
@@ -23,9 +21,7 @@ const config: SqlxJsConfig = {
|
|
|
23
21
|
},
|
|
24
22
|
jsonbTypes: {},
|
|
25
23
|
customTypes: {},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default config;
|
|
24
|
+
});
|
|
29
25
|
`;
|
|
30
26
|
const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
|
|
31
27
|
DATABASE_URL=postgres://user:password@localhost:5432/your_db
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { PgClient, parseDatabaseUrl, decodeText } from "../pg/wire.js";
|
|
7
|
-
import { openSession, prepareOnce } from "./prepare.js";
|
|
7
|
+
import { openSession, prepareOnce, verifyPrepareArtifacts } from "./prepare.js";
|
|
8
8
|
import { introspectConnected, schemaSnapshotEqual, } from "../schema-snapshot.js";
|
|
9
9
|
const SQUASH_PREFIX = "-- sqlx-js-squash:";
|
|
10
10
|
export const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
|
|
@@ -512,31 +512,16 @@ async function prepareWorkflowArtifacts(opts, databaseUrl) {
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
async function prepareInTemporaryArtifacts(opts, databaseUrl) {
|
|
515
|
-
const
|
|
516
|
-
const cacheDir = join(tmp, "cache");
|
|
517
|
-
const dtsPath = join(tmp, "sqlx-js-env.d.ts");
|
|
518
|
-
const prepareOpts = {
|
|
515
|
+
const verification = await verifyPrepareArtifacts({
|
|
519
516
|
root: opts.root,
|
|
520
517
|
databaseUrl,
|
|
521
|
-
cacheDir,
|
|
522
|
-
dtsPath,
|
|
518
|
+
cacheDir: opts.cacheDir,
|
|
519
|
+
dtsPath: opts.dtsPath,
|
|
523
520
|
check: false,
|
|
521
|
+
verify: true,
|
|
524
522
|
prune: true,
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
try {
|
|
528
|
-
const r = await prepareOnce(prepareOpts, session);
|
|
529
|
-
if (r.failures > 0) {
|
|
530
|
-
console.error(`\n${r.failures} query/queries failed to prepare`);
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
console.log(`shadow: prepared ${r.entries} unique query/queries`);
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
536
|
-
finally {
|
|
537
|
-
await session.client.end();
|
|
538
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
539
|
-
}
|
|
523
|
+
});
|
|
524
|
+
return verification.ok;
|
|
540
525
|
}
|
|
541
526
|
function effectiveSquashReplacements(all) {
|
|
542
527
|
const covered = squashCoveredVersions(all);
|
|
@@ -19,7 +19,13 @@ export type PgschemaInstallOptions = {
|
|
|
19
19
|
baseUrl?: string;
|
|
20
20
|
log?: (msg: string) => void;
|
|
21
21
|
};
|
|
22
|
+
export type PgschemaProbe = {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
command?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
22
27
|
export declare function resolvePgschemaAsset(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): PgschemaAsset;
|
|
23
28
|
export declare function managedPgschemaPath(root: string, asset?: PgschemaAsset): string;
|
|
29
|
+
export declare function probePgschema(root: string, config: SqlxJsConfig): PgschemaProbe;
|
|
24
30
|
export declare function runPgschemaInstall(opts: PgschemaInstallOptions): Promise<void>;
|
|
25
31
|
export declare function runPgschemaCommand(opts: PgschemaCommandOptions): void;
|
|
@@ -73,6 +73,36 @@ function commandName(root, config) {
|
|
|
73
73
|
return managed;
|
|
74
74
|
return "pgschema";
|
|
75
75
|
}
|
|
76
|
+
export function probePgschema(root, config) {
|
|
77
|
+
try {
|
|
78
|
+
const schema = pgschemaConfig(config);
|
|
79
|
+
const command = commandName(root, schema);
|
|
80
|
+
const child = spawnSync(command, ["--help"], {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
env: process.env,
|
|
83
|
+
timeout: 5_000,
|
|
84
|
+
});
|
|
85
|
+
if (child.error) {
|
|
86
|
+
const code = child.error.code;
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
command,
|
|
90
|
+
message: code === "ENOENT"
|
|
91
|
+
? installHint(command)
|
|
92
|
+
: code === "ETIMEDOUT"
|
|
93
|
+
? `${command} --help timed out after 5000ms`
|
|
94
|
+
: child.error.message,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (child.status !== 0) {
|
|
98
|
+
return { ok: false, command, message: `${command} --help exited with ${child.status}` };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, command, message: `pgschema is available: ${command}` };
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return { ok: false, message: e.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
76
106
|
function schemaFile(root, config) {
|
|
77
107
|
return resolve(root, config.file ?? "schema.sql");
|
|
78
108
|
}
|
|
@@ -7,8 +7,42 @@ export type PrepareOptions = {
|
|
|
7
7
|
cacheDir: string;
|
|
8
8
|
dtsPath: string;
|
|
9
9
|
check: boolean;
|
|
10
|
+
verify?: boolean;
|
|
11
|
+
json?: boolean;
|
|
10
12
|
prune?: boolean;
|
|
11
13
|
};
|
|
14
|
+
export type PrepareDiagnosticPhase = "config" | "connect" | "shadow" | "scan" | "describe" | "introspect" | "analyze" | "param-map" | "cache" | "verify";
|
|
15
|
+
export declare class PrepareFatalError extends Error {
|
|
16
|
+
readonly phase: PrepareDiagnosticPhase;
|
|
17
|
+
readonly file?: string;
|
|
18
|
+
readonly line?: number;
|
|
19
|
+
readonly column?: number;
|
|
20
|
+
constructor(phase: PrepareDiagnosticPhase, message: string, location?: {
|
|
21
|
+
file?: string;
|
|
22
|
+
line?: number;
|
|
23
|
+
column?: number;
|
|
24
|
+
}, options?: ErrorOptions);
|
|
25
|
+
}
|
|
26
|
+
export type PrepareDiagnostic = {
|
|
27
|
+
severity: "error" | "warning";
|
|
28
|
+
phase: PrepareDiagnosticPhase;
|
|
29
|
+
message: string;
|
|
30
|
+
file?: string;
|
|
31
|
+
line?: number;
|
|
32
|
+
column?: number;
|
|
33
|
+
query?: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
position?: number;
|
|
36
|
+
hint?: string;
|
|
37
|
+
};
|
|
38
|
+
export type PrepareResult = {
|
|
39
|
+
sites: number;
|
|
40
|
+
entries: number;
|
|
41
|
+
failures: number;
|
|
42
|
+
pruned: number;
|
|
43
|
+
functions: number;
|
|
44
|
+
diagnostics: PrepareDiagnostic[];
|
|
45
|
+
};
|
|
12
46
|
export type PrepareSession = {
|
|
13
47
|
client: PgClient;
|
|
14
48
|
schema: SchemaCache;
|
|
@@ -28,11 +62,11 @@ export declare function describeAll(cfg: ConnConfig, sessionClient: PgClient, qu
|
|
|
28
62
|
fp: string;
|
|
29
63
|
query: string;
|
|
30
64
|
}[], concurrency: number): Promise<Map<string, DescribeOutcome>>;
|
|
31
|
-
export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void, concurrency?: number): Promise<
|
|
32
|
-
entries: number;
|
|
33
|
-
failures: number;
|
|
34
|
-
pruned: number;
|
|
35
|
-
functions: number;
|
|
36
|
-
}>;
|
|
65
|
+
export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void, concurrency?: number): Promise<PrepareResult>;
|
|
37
66
|
export declare function runPrepare(opts: PrepareOptions): Promise<void>;
|
|
67
|
+
export declare function verifyPrepareArtifacts(opts: PrepareOptions, log?: (msg: string) => void, err?: (msg: string) => void): Promise<{
|
|
68
|
+
ok: boolean;
|
|
69
|
+
result: PrepareResult;
|
|
70
|
+
changed: string[];
|
|
71
|
+
}>;
|
|
38
72
|
export {};
|