@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
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
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, 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
|
|
@@ -37,10 +33,23 @@ const PGSCHEMA_TEMPLATE = `-- Desired PostgreSQL schema managed by pgschema.
|
|
|
37
33
|
-- sqlx-js db plan -- --output-json plan.json
|
|
38
34
|
-- sqlx-js db apply -- --auto-approve
|
|
39
35
|
`;
|
|
36
|
+
const DTS_TEMPLATE = `// AUTO-GENERATED by sqlx-js. Do not edit.
|
|
37
|
+
// Run \`sqlx-js prepare\` to regenerate.
|
|
38
|
+
|
|
39
|
+
declare module "@onreza/sqlx-js" {
|
|
40
|
+
interface KnownQueries {}
|
|
41
|
+
interface KnownFileQueries {}
|
|
42
|
+
interface KnownFunctions {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {};
|
|
46
|
+
`;
|
|
40
47
|
export function runInit(opts) {
|
|
41
48
|
const log = opts.log ?? console.log;
|
|
42
49
|
const created = [];
|
|
50
|
+
const updated = [];
|
|
43
51
|
const skipped = [];
|
|
52
|
+
const notes = [];
|
|
44
53
|
const ensureFile = (rel, content) => {
|
|
45
54
|
const full = join(opts.root, rel);
|
|
46
55
|
if (existsSync(full)) {
|
|
@@ -67,10 +76,81 @@ export function runInit(opts) {
|
|
|
67
76
|
else
|
|
68
77
|
ensureDir("migrations");
|
|
69
78
|
ensureFile(".env.example", ENV_TEMPLATE);
|
|
79
|
+
ensureFile("sqlx-js-env.d.ts", DTS_TEMPLATE);
|
|
80
|
+
const updateJson = (rel, mutate) => {
|
|
81
|
+
const full = join(opts.root, rel);
|
|
82
|
+
if (!existsSync(full))
|
|
83
|
+
return;
|
|
84
|
+
const source = readFileSync(full, "utf8");
|
|
85
|
+
let value;
|
|
86
|
+
try {
|
|
87
|
+
value = JSON.parse(source);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
notes.push(`${rel}: manual update required because the file is not strict JSON`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
94
|
+
return;
|
|
95
|
+
if (!mutate(value))
|
|
96
|
+
return;
|
|
97
|
+
const indent = source.match(/\n([ \t]+)"/)?.[1] ?? " ";
|
|
98
|
+
writeFileSync(full, JSON.stringify(value, null, indent) + "\n");
|
|
99
|
+
updated.push(rel);
|
|
100
|
+
};
|
|
101
|
+
updateJson("tsconfig.json", (value) => {
|
|
102
|
+
const files = value.files;
|
|
103
|
+
if (files !== undefined && (!Array.isArray(files) || !files.every((item) => typeof item === "string"))) {
|
|
104
|
+
notes.push("tsconfig.json: manual update required because files is not an array of strings");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(files)) {
|
|
108
|
+
if (files.includes("sqlx-js-env.d.ts"))
|
|
109
|
+
return false;
|
|
110
|
+
files.push("sqlx-js-env.d.ts");
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const include = value.include;
|
|
114
|
+
if (include !== undefined && (!Array.isArray(include) || !include.every((item) => typeof item === "string"))) {
|
|
115
|
+
notes.push("tsconfig.json: manual update required because include is not an array of strings");
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(include)) {
|
|
119
|
+
if (include.includes("sqlx-js-env.d.ts"))
|
|
120
|
+
return false;
|
|
121
|
+
include.push("sqlx-js-env.d.ts");
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
updateJson("package.json", (value) => {
|
|
127
|
+
if (value.scripts !== undefined && (!value.scripts || typeof value.scripts !== "object" || Array.isArray(value.scripts))) {
|
|
128
|
+
notes.push("package.json: manual script update required because scripts is not an object");
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const scripts = (value.scripts ??= {});
|
|
132
|
+
const defaults = {
|
|
133
|
+
"sqlx:prepare": "sqlx-js prepare",
|
|
134
|
+
"sqlx:check": "sqlx-js prepare --check",
|
|
135
|
+
"sqlx:verify": "sqlx-js prepare --verify --strict-inference",
|
|
136
|
+
};
|
|
137
|
+
let changed = false;
|
|
138
|
+
for (const [name, command] of Object.entries(defaults)) {
|
|
139
|
+
if (scripts[name] !== undefined)
|
|
140
|
+
continue;
|
|
141
|
+
scripts[name] = command;
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
return changed;
|
|
145
|
+
});
|
|
70
146
|
for (const f of created)
|
|
71
147
|
log(`created ${f}`);
|
|
148
|
+
for (const f of updated)
|
|
149
|
+
log(`updated ${f}`);
|
|
72
150
|
for (const f of skipped)
|
|
73
151
|
log(`exists ${f} (left unchanged)`);
|
|
152
|
+
for (const note of notes)
|
|
153
|
+
log(`manual ${note}`);
|
|
74
154
|
log("");
|
|
75
155
|
log("Next steps:");
|
|
76
156
|
log(" 1. Set DATABASE_URL (see .env.example).");
|
|
@@ -82,7 +162,7 @@ export function runInit(opts) {
|
|
|
82
162
|
}
|
|
83
163
|
else {
|
|
84
164
|
log(" 2. Add a migration: sqlx-js migrate add init");
|
|
85
|
-
log(" 3.
|
|
86
|
-
log(" 4. Develop locally: sqlx-js migrate dev");
|
|
165
|
+
log(" 3. Check the project setup: sqlx-js doctor");
|
|
166
|
+
log(" 4. Develop locally: sqlx-js migrate dev --strict-inference");
|
|
87
167
|
}
|
|
88
168
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PgClient } from "../pg/wire.js";
|
|
2
|
+
export * from "../migration-core.js";
|
|
2
3
|
export type MigrateOptions = {
|
|
3
4
|
databaseUrl: string;
|
|
4
5
|
migrationsDir: string;
|
|
@@ -12,91 +13,7 @@ export type MigrationWorkflowOptions = MigrateOptions & {
|
|
|
12
13
|
shadowAdminUrl?: string;
|
|
13
14
|
lockKey?: number | bigint;
|
|
14
15
|
lockTimeoutMs?: number;
|
|
15
|
-
|
|
16
|
-
export type MigrationStore = {
|
|
17
|
-
table: string;
|
|
18
|
-
};
|
|
19
|
-
export declare const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
|
|
20
|
-
export declare function ensureTable(c: PgClient): Promise<MigrationStore>;
|
|
21
|
-
export declare function listApplied(c: PgClient, store?: MigrationStore): Promise<Map<number, {
|
|
22
|
-
name: string;
|
|
23
|
-
hash: string;
|
|
24
|
-
}>>;
|
|
25
|
-
export type ApplyOutcome = {
|
|
26
|
-
kind: "applied";
|
|
27
|
-
version: number;
|
|
28
|
-
name: string;
|
|
29
|
-
} | {
|
|
30
|
-
kind: "adopted";
|
|
31
|
-
version: number;
|
|
32
|
-
name: string;
|
|
33
|
-
replaced: number;
|
|
34
|
-
} | {
|
|
35
|
-
kind: "tampered";
|
|
36
|
-
version: number;
|
|
37
|
-
name: string;
|
|
38
|
-
applied: string;
|
|
39
|
-
current: string;
|
|
40
|
-
} | {
|
|
41
|
-
kind: "failed";
|
|
42
|
-
version: number;
|
|
43
|
-
name: string;
|
|
44
|
-
error: string;
|
|
45
|
-
};
|
|
46
|
-
export type PlanOutcome = {
|
|
47
|
-
kind: "pending";
|
|
48
|
-
version: number;
|
|
49
|
-
name: string;
|
|
50
|
-
} | {
|
|
51
|
-
kind: "adoptable";
|
|
52
|
-
version: number;
|
|
53
|
-
name: string;
|
|
54
|
-
replaced: number;
|
|
55
|
-
} | {
|
|
56
|
-
kind: "tampered";
|
|
57
|
-
version: number;
|
|
58
|
-
name: string;
|
|
59
|
-
applied: string;
|
|
60
|
-
current: string;
|
|
61
|
-
} | {
|
|
62
|
-
kind: "failed";
|
|
63
|
-
version: number;
|
|
64
|
-
name: string;
|
|
65
|
-
error: string;
|
|
66
|
-
};
|
|
67
|
-
export type MigrationPlanItem = {
|
|
68
|
-
kind: "apply";
|
|
69
|
-
version: number;
|
|
70
|
-
name: string;
|
|
71
|
-
} | {
|
|
72
|
-
kind: "adopt";
|
|
73
|
-
version: number;
|
|
74
|
-
name: string;
|
|
75
|
-
replaced: number;
|
|
76
|
-
};
|
|
77
|
-
export type MigrationPlanDiagnostic = Extract<PlanOutcome, {
|
|
78
|
-
kind: "tampered" | "failed";
|
|
79
|
-
}>;
|
|
80
|
-
export type MigrationPlanSnapshot = {
|
|
81
|
-
ok: boolean;
|
|
82
|
-
pending: number;
|
|
83
|
-
adoptable: number;
|
|
84
|
-
tampered: number;
|
|
85
|
-
failed: number;
|
|
86
|
-
steps: MigrationPlanItem[];
|
|
87
|
-
diagnostics: MigrationPlanDiagnostic[];
|
|
88
|
-
};
|
|
89
|
-
export type MigrationInfoStatus = "applied" | "pending" | "adoptable" | "superseded" | "tampered" | "failed";
|
|
90
|
-
export type MigrationInfoItem = {
|
|
91
|
-
version: number;
|
|
92
|
-
name: string;
|
|
93
|
-
status: MigrationInfoStatus;
|
|
94
|
-
detail?: string;
|
|
95
|
-
};
|
|
96
|
-
export type MigrationInfoSnapshot = {
|
|
97
|
-
historyTable: string | null;
|
|
98
|
-
summary: Record<MigrationInfoStatus, number>;
|
|
99
|
-
items: MigrationInfoItem[];
|
|
16
|
+
strictInference?: boolean;
|
|
100
17
|
};
|
|
101
18
|
export type MigrationArchive = {
|
|
102
19
|
name: string;
|
|
@@ -151,22 +68,6 @@ export type RevertDryRunOutcome = {
|
|
|
151
68
|
error: string;
|
|
152
69
|
};
|
|
153
70
|
export declare function checkMigrationFiles(migrationsDir: string): MigrationCheckReport;
|
|
154
|
-
export declare function planPending(c: PgClient, migrationsDir: string, onEvent?: (e: PlanOutcome) => void): Promise<{
|
|
155
|
-
pending: number;
|
|
156
|
-
adoptable: number;
|
|
157
|
-
tampered: number;
|
|
158
|
-
failed: number;
|
|
159
|
-
steps: MigrationPlanItem[];
|
|
160
|
-
}>;
|
|
161
|
-
export declare function inspectMigrationPlan(c: PgClient, migrationsDir: string): Promise<MigrationPlanSnapshot>;
|
|
162
|
-
export declare function inspectMigrations(c: PgClient, migrationsDir: string): Promise<MigrationInfoSnapshot>;
|
|
163
|
-
export declare function applyPending(c: PgClient, migrationsDir: string, onEvent?: (e: ApplyOutcome) => void): Promise<{
|
|
164
|
-
applied: number;
|
|
165
|
-
tampered: number;
|
|
166
|
-
failed: number;
|
|
167
|
-
}>;
|
|
168
|
-
export declare function acquireMigrateLock(c: PgClient, lockKey?: number | bigint, timeoutMs?: number): Promise<void>;
|
|
169
|
-
export declare function releaseMigrateLock(c: PgClient, lockKey?: number | bigint): Promise<void>;
|
|
170
71
|
export declare function sanitizePgDumpSchema(sql: string): string;
|
|
171
72
|
export declare function listMigrationArchives(migrationsDir: string): MigrationArchive[];
|
|
172
73
|
export declare function restoreMigrationArchive(migrationsDir: string, archiveName: string, opts?: {
|