@onreza/sqlx-js 0.3.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 +129 -36
- package/ROADMAP.md +3 -4
- package/dist/bin/sqlx-js.js +137 -26
- 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.d.ts +2 -1
- package/dist/src/codegen.js +34 -5
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +196 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +36 -9
- package/dist/src/commands/migrate.js +7 -22
- package/dist/src/commands/pgschema.d.ts +31 -0
- package/dist/src/commands/pgschema.js +208 -0
- package/dist/src/commands/prepare.d.ts +40 -5
- package/dist/src/commands/prepare.js +344 -58
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +27 -0
- package/dist/src/config.js +141 -11
- package/dist/src/function-cache.d.ts +18 -0
- package/dist/src/function-cache.js +38 -0
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.js +2 -1
- package/dist/src/pg/functions.d.ts +4 -0
- package/dist/src/pg/functions.js +150 -0
- package/dist/src/pg/oids.js +2 -0
- package/dist/src/pg/schema.d.ts +1 -0
- package/dist/src/pg/schema.js +3 -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 +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
|
@@ -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,23 +1,38 @@
|
|
|
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
|
-
};
|
|
12
|
+
});
|
|
13
|
+
`;
|
|
14
|
+
const PGSCHEMA_CONFIG_TEMPLATE = `import { defineConfig } from "@onreza/sqlx-js";
|
|
13
15
|
|
|
14
|
-
export default
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
schema: {
|
|
18
|
+
provider: "pgschema",
|
|
19
|
+
file: "schema.sql",
|
|
20
|
+
schemas: ["public"],
|
|
21
|
+
},
|
|
22
|
+
jsonbTypes: {},
|
|
23
|
+
customTypes: {},
|
|
24
|
+
});
|
|
15
25
|
`;
|
|
16
26
|
const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
|
|
17
27
|
DATABASE_URL=postgres://user:password@localhost:5432/your_db
|
|
18
28
|
# Managed Postgres with TLS:
|
|
19
29
|
# DATABASE_URL=postgres://user:password@db.example.com:5432/your_db?sslmode=verify-full
|
|
20
30
|
`;
|
|
31
|
+
const PGSCHEMA_TEMPLATE = `-- Desired PostgreSQL schema managed by pgschema.
|
|
32
|
+
-- Edit this file, then run:
|
|
33
|
+
-- sqlx-js db plan -- --output-json plan.json
|
|
34
|
+
-- sqlx-js db apply -- --auto-approve
|
|
35
|
+
`;
|
|
21
36
|
export function runInit(opts) {
|
|
22
37
|
const log = opts.log ?? console.log;
|
|
23
38
|
const created = [];
|
|
@@ -41,8 +56,12 @@ export function runInit(opts) {
|
|
|
41
56
|
mkdirSync(full, { recursive: true });
|
|
42
57
|
created.push(`${rel}/`);
|
|
43
58
|
};
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
const schemaProvider = opts.schemaProvider ?? "builtin";
|
|
60
|
+
ensureFile("sqlx-js.config.ts", schemaProvider === "pgschema" ? PGSCHEMA_CONFIG_TEMPLATE : CONFIG_TEMPLATE);
|
|
61
|
+
if (schemaProvider === "pgschema")
|
|
62
|
+
ensureFile("schema.sql", PGSCHEMA_TEMPLATE);
|
|
63
|
+
else
|
|
64
|
+
ensureDir("migrations");
|
|
46
65
|
ensureFile(".env.example", ENV_TEMPLATE);
|
|
47
66
|
for (const f of created)
|
|
48
67
|
log(`created ${f}`);
|
|
@@ -51,7 +70,15 @@ export function runInit(opts) {
|
|
|
51
70
|
log("");
|
|
52
71
|
log("Next steps:");
|
|
53
72
|
log(" 1. Set DATABASE_URL (see .env.example).");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
if (schemaProvider === "pgschema") {
|
|
74
|
+
log(" 2. Install managed pgschema: sqlx-js db install");
|
|
75
|
+
log(" 3. Verify it: sqlx-js db check");
|
|
76
|
+
log(" 4. Edit schema.sql, then review: sqlx-js db plan");
|
|
77
|
+
log(" 5. After applying schema changes, run: sqlx-js prepare");
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
log(" 2. Add a migration: sqlx-js migrate add init");
|
|
81
|
+
log(" 3. Make sure tsconfig.json \"include\" covers sqlx-js-env.d.ts.");
|
|
82
|
+
log(" 4. Develop locally: sqlx-js migrate dev");
|
|
83
|
+
}
|
|
57
84
|
}
|
|
@@ -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);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SqlxJsConfig } from "../config.js";
|
|
2
|
+
export type PgschemaSubcommand = "check" | "plan" | "apply";
|
|
3
|
+
export declare const PGSCHEMA_VERSION = "1.12.0";
|
|
4
|
+
export type PgschemaAsset = {
|
|
5
|
+
key: string;
|
|
6
|
+
name: string;
|
|
7
|
+
sha256: string;
|
|
8
|
+
};
|
|
9
|
+
export type PgschemaCommandOptions = {
|
|
10
|
+
root: string;
|
|
11
|
+
databaseUrl: string;
|
|
12
|
+
config: SqlxJsConfig;
|
|
13
|
+
subcommand: PgschemaSubcommand;
|
|
14
|
+
passthrough?: string[];
|
|
15
|
+
};
|
|
16
|
+
export type PgschemaInstallOptions = {
|
|
17
|
+
root: string;
|
|
18
|
+
asset?: PgschemaAsset;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
log?: (msg: string) => void;
|
|
21
|
+
};
|
|
22
|
+
export type PgschemaProbe = {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
command?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function resolvePgschemaAsset(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): PgschemaAsset;
|
|
28
|
+
export declare function managedPgschemaPath(root: string, asset?: PgschemaAsset): string;
|
|
29
|
+
export declare function probePgschema(root: string, config: SqlxJsConfig): PgschemaProbe;
|
|
30
|
+
export declare function runPgschemaInstall(opts: PgschemaInstallOptions): Promise<void>;
|
|
31
|
+
export declare function runPgschemaCommand(opts: PgschemaCommandOptions): void;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { parseDatabaseUrl } from "../pg/wire.js";
|
|
6
|
+
export const PGSCHEMA_VERSION = "1.12.0";
|
|
7
|
+
const PGSCHEMA_BASE_URL = `https://github.com/pgplex/pgschema/releases/download/v${PGSCHEMA_VERSION}`;
|
|
8
|
+
const WINDOWS_UNSUPPORTED = "sqlx-js db: pgschema is not supported on Windows. Run sqlx-js under WSL/Linux/macOS or use the built-in sqlx-js migrate workflow.";
|
|
9
|
+
const PGSCHEMA_ASSETS = {
|
|
10
|
+
"darwin:x64": {
|
|
11
|
+
key: "darwin-amd64",
|
|
12
|
+
name: `pgschema-${PGSCHEMA_VERSION}-darwin-amd64`,
|
|
13
|
+
sha256: "c64b2ac24c4246344908910e892c4123be282bbb449f0b535079ff41d0f47c8f",
|
|
14
|
+
},
|
|
15
|
+
"darwin:arm64": {
|
|
16
|
+
key: "darwin-arm64",
|
|
17
|
+
name: `pgschema-${PGSCHEMA_VERSION}-darwin-arm64`,
|
|
18
|
+
sha256: "f01ea488f21700752d5747bc013c406daa583a68b631739f33af430d5d3ec449",
|
|
19
|
+
},
|
|
20
|
+
"linux:x64": {
|
|
21
|
+
key: "linux-amd64",
|
|
22
|
+
name: `pgschema-${PGSCHEMA_VERSION}-linux-amd64`,
|
|
23
|
+
sha256: "12610adf748b0dafe4e488ee7e9e68e6ffbef1f4e0f038dda36cf0138eede598",
|
|
24
|
+
},
|
|
25
|
+
"linux:arm64": {
|
|
26
|
+
key: "linux-arm64",
|
|
27
|
+
name: `pgschema-${PGSCHEMA_VERSION}-linux-arm64`,
|
|
28
|
+
sha256: "58ec57023954a0239cf9d607c4e5432da6dd0b279399d1c318204120619a221d",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function resolvePgschemaAsset(platform = process.platform, arch = process.arch) {
|
|
32
|
+
if (platform === "win32")
|
|
33
|
+
throw new Error(WINDOWS_UNSUPPORTED);
|
|
34
|
+
const asset = PGSCHEMA_ASSETS[`${platform}:${arch}`];
|
|
35
|
+
if (!asset)
|
|
36
|
+
throw new Error(`sqlx-js db install: unsupported platform ${platform}/${arch}`);
|
|
37
|
+
return asset;
|
|
38
|
+
}
|
|
39
|
+
function pgschemaConfig(config) {
|
|
40
|
+
if (config.schema?.provider !== "pgschema") {
|
|
41
|
+
throw new Error("sqlx-js db: set schema.provider = \"pgschema\" in sqlx-js.config.ts");
|
|
42
|
+
}
|
|
43
|
+
return config.schema;
|
|
44
|
+
}
|
|
45
|
+
export function managedPgschemaPath(root, asset = resolvePgschemaAsset()) {
|
|
46
|
+
return join(root, "node_modules/.cache/sqlx-js/pgschema", `v${PGSCHEMA_VERSION}`, asset.key, "pgschema");
|
|
47
|
+
}
|
|
48
|
+
function maybeManagedPgschemaPath(root) {
|
|
49
|
+
try {
|
|
50
|
+
const asset = resolvePgschemaAsset();
|
|
51
|
+
const managed = managedPgschemaPath(root, asset);
|
|
52
|
+
if (!existsSync(managed))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (sha256(readFileSync(managed)) !== asset.sha256) {
|
|
55
|
+
throw new Error(`sqlx-js db: managed pgschema checksum mismatch at ${managed}. Run sqlx-js db install.`);
|
|
56
|
+
}
|
|
57
|
+
chmodSync(managed, 0o755);
|
|
58
|
+
return managed;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e.message.includes("checksum mismatch"))
|
|
62
|
+
throw e;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function commandName(root, config) {
|
|
67
|
+
if (process.platform === "win32")
|
|
68
|
+
throw new Error(WINDOWS_UNSUPPORTED);
|
|
69
|
+
if (config.command)
|
|
70
|
+
return config.command;
|
|
71
|
+
const managed = maybeManagedPgschemaPath(root);
|
|
72
|
+
if (managed && existsSync(managed))
|
|
73
|
+
return managed;
|
|
74
|
+
return "pgschema";
|
|
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
|
+
}
|
|
106
|
+
function schemaFile(root, config) {
|
|
107
|
+
return resolve(root, config.file ?? "schema.sql");
|
|
108
|
+
}
|
|
109
|
+
function appliesPlan(subcommand, passthrough) {
|
|
110
|
+
return subcommand === "apply" && (passthrough ?? []).some((arg) => arg === "--plan" || arg.startsWith("--plan="));
|
|
111
|
+
}
|
|
112
|
+
function installHint(command) {
|
|
113
|
+
return `sqlx-js db: ${command} was not found. Run sqlx-js db install or set schema.command in sqlx-js.config.ts.`;
|
|
114
|
+
}
|
|
115
|
+
function run(command, args, env) {
|
|
116
|
+
const child = spawnSync(command, args, { encoding: "utf8", env, stdio: "inherit" });
|
|
117
|
+
if (child.error) {
|
|
118
|
+
const code = child.error.code;
|
|
119
|
+
if (code === "ENOENT")
|
|
120
|
+
throw new Error(installHint(command));
|
|
121
|
+
throw child.error;
|
|
122
|
+
}
|
|
123
|
+
if (child.signal)
|
|
124
|
+
throw new Error(`sqlx-js db: ${command} terminated by signal ${child.signal}`);
|
|
125
|
+
if (child.status && child.status !== 0)
|
|
126
|
+
process.exit(child.status);
|
|
127
|
+
}
|
|
128
|
+
function pgschemaEnv(db) {
|
|
129
|
+
return {
|
|
130
|
+
...process.env,
|
|
131
|
+
...(db.password ? { PGPASSWORD: db.password } : {}),
|
|
132
|
+
...(db.sslmode ? { PGSSLMODE: db.sslmode } : {}),
|
|
133
|
+
...(db.sslRootCert ? { PGSSLROOTCERT: db.sslRootCert } : {}),
|
|
134
|
+
...(db.sslCert ? { PGSSLCERT: db.sslCert } : {}),
|
|
135
|
+
...(db.sslKey ? { PGSSLKEY: db.sslKey } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function sha256(data) {
|
|
139
|
+
return createHash("sha256").update(data).digest("hex");
|
|
140
|
+
}
|
|
141
|
+
export async function runPgschemaInstall(opts) {
|
|
142
|
+
const asset = opts.asset ?? resolvePgschemaAsset();
|
|
143
|
+
const baseUrl = opts.baseUrl ?? PGSCHEMA_BASE_URL;
|
|
144
|
+
const log = opts.log ?? console.log;
|
|
145
|
+
const target = managedPgschemaPath(opts.root, asset);
|
|
146
|
+
if (existsSync(target) && sha256(readFileSync(target)) === asset.sha256) {
|
|
147
|
+
chmodSync(target, 0o755);
|
|
148
|
+
log(`pgschema v${PGSCHEMA_VERSION} already installed at ${target}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${asset.name}`;
|
|
152
|
+
const response = await fetch(url);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw new Error(`sqlx-js db install: failed to download pgschema v${PGSCHEMA_VERSION}: HTTP ${response.status}`);
|
|
155
|
+
}
|
|
156
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
157
|
+
const actual = sha256(bytes);
|
|
158
|
+
if (actual !== asset.sha256) {
|
|
159
|
+
throw new Error(`sqlx-js db install: checksum mismatch for ${asset.name}`);
|
|
160
|
+
}
|
|
161
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
162
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
163
|
+
try {
|
|
164
|
+
writeFileSync(tmp, bytes, { mode: 0o755 });
|
|
165
|
+
chmodSync(tmp, 0o755);
|
|
166
|
+
renameSync(tmp, target);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
rmSync(tmp, { force: true });
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
172
|
+
writeFileSync(`${target}.json`, JSON.stringify({ version: PGSCHEMA_VERSION, asset: asset.name, sha256: asset.sha256 }, null, 2) + "\n");
|
|
173
|
+
log(`installed pgschema v${PGSCHEMA_VERSION} to ${target}`);
|
|
174
|
+
}
|
|
175
|
+
export function runPgschemaCommand(opts) {
|
|
176
|
+
const config = pgschemaConfig(opts.config);
|
|
177
|
+
const command = commandName(opts.root, config);
|
|
178
|
+
if (opts.subcommand === "check") {
|
|
179
|
+
run(command, ["--help"], process.env);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!opts.databaseUrl)
|
|
183
|
+
throw new Error("DATABASE_URL is required for sqlx-js db commands");
|
|
184
|
+
const file = appliesPlan(opts.subcommand, opts.passthrough) ? undefined : schemaFile(opts.root, config);
|
|
185
|
+
if (file && !existsSync(file))
|
|
186
|
+
throw new Error(`sqlx-js db: schema file not found: ${file}`);
|
|
187
|
+
const db = parseDatabaseUrl(opts.databaseUrl);
|
|
188
|
+
const schemas = pgschemaSchemas(config);
|
|
189
|
+
const args = [
|
|
190
|
+
opts.subcommand,
|
|
191
|
+
"--host", db.host,
|
|
192
|
+
"--port", String(db.port),
|
|
193
|
+
"--db", db.database,
|
|
194
|
+
"--user", db.user,
|
|
195
|
+
];
|
|
196
|
+
if (file)
|
|
197
|
+
args.push("--file", file);
|
|
198
|
+
args.push("--schema", schemas[0]);
|
|
199
|
+
args.push(...opts.passthrough ?? []);
|
|
200
|
+
run(command, args, pgschemaEnv(db));
|
|
201
|
+
}
|
|
202
|
+
function pgschemaSchemas(config) {
|
|
203
|
+
const schemas = config.schemas?.length ? config.schemas : ["public"];
|
|
204
|
+
if (schemas.length > 1) {
|
|
205
|
+
throw new Error("sqlx-js db: pgschema 1.12.0 supports exactly one --schema value; split plan/apply per schema or use a single schema in sqlx-js.config.ts");
|
|
206
|
+
}
|
|
207
|
+
return schemas;
|
|
208
|
+
}
|
|
@@ -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,10 +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
|
-
}>;
|
|
65
|
+
export declare function prepareOnce(opts: PrepareOptions, session: PrepareSession, log?: (msg: string) => void, err?: (msg: string) => void, concurrency?: number): Promise<PrepareResult>;
|
|
36
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
|
+
}>;
|
|
37
72
|
export {};
|