pgpm 5.19.1 → 5.20.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/commands/cache.js +1 -1
- package/commands/diff.d.ts +3 -0
- package/commands/diff.js +325 -0
- package/commands/dump.js +3 -3
- package/commands/export.js +24 -5
- package/commands/import.d.ts +3 -0
- package/commands/import.js +198 -0
- package/commands/init/boilerplate.js +1 -1
- package/commands/init/index.js +3 -3
- package/commands/package.js +0 -1
- package/commands/slice.js +1 -1
- package/commands/test-packages.js +1 -1
- package/commands/transform.d.ts +13 -0
- package/commands/transform.js +275 -0
- package/commands/update.js +1 -1
- package/commands.js +7 -0
- package/esm/commands/cache.js +1 -1
- package/esm/commands/diff.js +290 -0
- package/esm/commands/dump.js +3 -3
- package/esm/commands/export.js +22 -3
- package/esm/commands/import.js +163 -0
- package/esm/commands/init/boilerplate.js +1 -1
- package/esm/commands/init/index.js +3 -3
- package/esm/commands/package.js +0 -1
- package/esm/commands/slice.js +1 -1
- package/esm/commands/test-packages.js +1 -1
- package/esm/commands/transform.js +236 -0
- package/esm/commands/update.js +1 -1
- package/esm/commands.js +7 -0
- package/esm/utils/display.js +3 -0
- package/esm/utils/driver.js +1 -1
- package/esm/utils/emit-package.js +1 -0
- package/esm/utils/engine.js +1 -1
- package/esm/utils/module-projections.js +47 -0
- package/esm/utils/scratch-db.js +61 -0
- package/package.json +14 -12
- package/utils/display.d.ts +1 -1
- package/utils/display.js +3 -0
- package/utils/driver.js +1 -1
- package/utils/emit-package.d.ts +9 -0
- package/utils/emit-package.js +7 -0
- package/utils/engine.js +1 -1
- package/utils/module-projections.d.ts +15 -0
- package/utils/module-projections.js +85 -0
- package/utils/scratch-db.d.ts +31 -0
- package/utils/scratch-db.js +66 -0
package/commands/slice.js
CHANGED
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const core_1 = require("@pgpmjs/core");
|
|
4
4
|
const slice_1 = require("@pgpmjs/slice");
|
|
5
5
|
const types_1 = require("@pgpmjs/types");
|
|
6
|
-
const path_1 = require("path");
|
|
7
6
|
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
8
|
const sliceUsageText = `
|
|
9
9
|
Slice Command:
|
|
10
10
|
|
|
@@ -7,8 +7,8 @@ const core_1 = require("@pgpmjs/core");
|
|
|
7
7
|
const env_1 = require("@pgpmjs/env");
|
|
8
8
|
const logger_1 = require("@pgpmjs/logger");
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const pg_env_1 = require("pg-env");
|
|
11
10
|
const pg_cache_1 = require("pg-cache");
|
|
11
|
+
const pg_env_1 = require("pg-env");
|
|
12
12
|
const log = new logger_1.Logger('test-packages');
|
|
13
13
|
// ANSI color codes
|
|
14
14
|
const RED = '\x1b[0;31m';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PartitionedPackageRows } from '@pgpmjs/transform';
|
|
2
|
+
import { CLIOptions, Inquirerer, ParsedArgs } from 'inquirerer';
|
|
3
|
+
export { checkOverwrite } from '../utils/emit-package';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the base directory package dir(s) are created in. Without --out,
|
|
6
|
+
* packages are written as siblings of the source module — a plain transform
|
|
7
|
+
* of `my-mod` at granularity `object` lands in `../my-mod-object`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const resolveOutBase: (modulePath: string, out?: string) => string;
|
|
10
|
+
/** Order partition packages so prerequisites come before dependents. */
|
|
11
|
+
export declare const orderPackages: (packages: PartitionedPackageRows[]) => PartitionedPackageRows[];
|
|
12
|
+
declare const _default: (argv: Partial<ParsedArgs>, prompter: Inquirerer, _options: CLIOptions) => Promise<Partial<ParsedArgs>>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.orderPackages = exports.resolveOutBase = exports.checkOverwrite = void 0;
|
|
37
|
+
const core_1 = require("@pgpmjs/core");
|
|
38
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
39
|
+
const transform_1 = require("@pgpmjs/transform");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const inquirerer_1 = require("inquirerer");
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const pg_env_1 = require("pg-env");
|
|
44
|
+
const emit_package_1 = require("../utils/emit-package");
|
|
45
|
+
const scratch_db_1 = require("../utils/scratch-db");
|
|
46
|
+
var emit_package_2 = require("../utils/emit-package");
|
|
47
|
+
Object.defineProperty(exports, "checkOverwrite", { enumerable: true, get: function () { return emit_package_2.checkOverwrite; } });
|
|
48
|
+
const log = new logger_1.Logger('transform');
|
|
49
|
+
const transformUsageText = `
|
|
50
|
+
Transform Command:
|
|
51
|
+
|
|
52
|
+
pgpm transform --granularity <atomic|object|consolidated> [OPTIONS]
|
|
53
|
+
|
|
54
|
+
Re-dial an existing pgpm module (or every module in a workspace) through the
|
|
55
|
+
dials pipeline: flatten the deploy scripts in plan order, re-project them at
|
|
56
|
+
the requested granularity with spec-derived change paths, graph-derived
|
|
57
|
+
requires, and generated revert/verify scripts.
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--help, -h Show this help message
|
|
61
|
+
--granularity <level> Target granularity: atomic | object | consolidated (required)
|
|
62
|
+
--partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
|
|
63
|
+
splitting the module into multiple pgpm packages with
|
|
64
|
+
derived cross-package requires.
|
|
65
|
+
--naming <style> Change path naming style: directory | flat (default: directory)
|
|
66
|
+
--cwd <directory> Module or workspace directory (default: current directory)
|
|
67
|
+
--out <dir> Output directory (default: sibling <module>-<granularity>)
|
|
68
|
+
--write Allow writing over an existing/module directory
|
|
69
|
+
--check Deploy original and transformed output into scratch
|
|
70
|
+
databases and assert the catalogs are equivalent
|
|
71
|
+
--dry-run Print the resulting plan/paths without writing
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
pgpm transform --granularity object
|
|
75
|
+
pgpm transform --granularity atomic --naming flat --out ./out
|
|
76
|
+
pgpm transform --granularity object --partition partition.json --check
|
|
77
|
+
`;
|
|
78
|
+
const NAMING_STYLES = ['directory', 'flat'];
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the base directory package dir(s) are created in. Without --out,
|
|
81
|
+
* packages are written as siblings of the source module — a plain transform
|
|
82
|
+
* of `my-mod` at granularity `object` lands in `../my-mod-object`.
|
|
83
|
+
*/
|
|
84
|
+
const resolveOutBase = (modulePath, out) => {
|
|
85
|
+
if (out)
|
|
86
|
+
return path.resolve(out);
|
|
87
|
+
return path.dirname(modulePath);
|
|
88
|
+
};
|
|
89
|
+
exports.resolveOutBase = resolveOutBase;
|
|
90
|
+
/** Order partition packages so prerequisites come before dependents. */
|
|
91
|
+
const orderPackages = (packages) => {
|
|
92
|
+
const byName = new Map(packages.map(pkg => [pkg.name, pkg]));
|
|
93
|
+
const ordered = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
const visit = (pkg) => {
|
|
96
|
+
if (seen.has(pkg.name))
|
|
97
|
+
return;
|
|
98
|
+
seen.add(pkg.name);
|
|
99
|
+
for (const req of pkg.requires) {
|
|
100
|
+
const dep = byName.get(req);
|
|
101
|
+
if (dep)
|
|
102
|
+
visit(dep);
|
|
103
|
+
}
|
|
104
|
+
ordered.push(pkg);
|
|
105
|
+
};
|
|
106
|
+
for (const pkg of packages)
|
|
107
|
+
visit(pkg);
|
|
108
|
+
return ordered;
|
|
109
|
+
};
|
|
110
|
+
exports.orderPackages = orderPackages;
|
|
111
|
+
const readControlRequires = (modulePath, name) => {
|
|
112
|
+
const controlPath = path.join(modulePath, `${name}.control`);
|
|
113
|
+
if (!fs.existsSync(controlPath))
|
|
114
|
+
return [];
|
|
115
|
+
const match = fs.readFileSync(controlPath, 'utf-8').match(/^requires\s*=\s*'([^']*)'\s*$/m);
|
|
116
|
+
if (!match)
|
|
117
|
+
return [];
|
|
118
|
+
return match[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
119
|
+
};
|
|
120
|
+
const deployModules = async (config, modulePaths) => {
|
|
121
|
+
const client = new core_1.PgpmMigrate(config);
|
|
122
|
+
for (const modulePath of modulePaths) {
|
|
123
|
+
const result = await client.deploy({ modulePath });
|
|
124
|
+
if (result.failed) {
|
|
125
|
+
throw new Error(`deploy failed at change ${result.failed} (module ${modulePath})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Prove the transform is structurally lossless: deploy the original module and
|
|
131
|
+
* the transformed package(s) into two scratch databases and compare catalogs.
|
|
132
|
+
*/
|
|
133
|
+
const runCheck = async (transformed) => {
|
|
134
|
+
const config = (0, pg_env_1.getPgEnvOptions)();
|
|
135
|
+
const stamp = Date.now();
|
|
136
|
+
const names = [`pgpm_transform_check_a_${stamp}`, `pgpm_transform_check_b_${stamp}`];
|
|
137
|
+
return (0, scratch_db_1.withScratchDatabases)(config, names, async ([cfgOriginal, cfgTransformed]) => {
|
|
138
|
+
await deployModules(cfgOriginal, [transformed.modulePath]);
|
|
139
|
+
const orderedDirs = (0, exports.orderPackages)(transformed.packages).map(pkg => path.join(transformed.outBase, pkg.name));
|
|
140
|
+
await deployModules(cfgTransformed, orderedDirs);
|
|
141
|
+
return (0, scratch_db_1.catalogDifferences)(cfgOriginal, cfgTransformed);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const transformModule = async (modulePath, granularity, naming, partition, out) => {
|
|
145
|
+
const source = (0, transform_1.loadModuleSource)(modulePath);
|
|
146
|
+
const warnings = [...source.warnings];
|
|
147
|
+
const rows = source.changes.map(change => ({
|
|
148
|
+
name: change.name,
|
|
149
|
+
deploy: change.name,
|
|
150
|
+
deps: change.dependencies,
|
|
151
|
+
content: change.deploy
|
|
152
|
+
}));
|
|
153
|
+
const restructured = await (0, transform_1.restructureExportRows)(rows, granularity, { naming });
|
|
154
|
+
warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
|
|
155
|
+
const outBase = (0, exports.resolveOutBase)(modulePath, out);
|
|
156
|
+
let packages;
|
|
157
|
+
if (partition) {
|
|
158
|
+
const result = await (0, transform_1.partitionExportRows)(restructured.rows, partition);
|
|
159
|
+
warnings.push(...result.warnings.map(w => `partition: ${w}`));
|
|
160
|
+
packages = result.packages;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Distinct package name so the output can live beside the source module
|
|
164
|
+
// in the same workspace without colliding.
|
|
165
|
+
packages = [{ name: `${source.name}-${granularity}`, requires: [], rows: restructured.rows }];
|
|
166
|
+
}
|
|
167
|
+
return { modulePath, name: source.name, outBase, packages, warnings };
|
|
168
|
+
};
|
|
169
|
+
const printDryRun = (transformed) => {
|
|
170
|
+
console.log(`module ${transformed.name} (${transformed.modulePath})`);
|
|
171
|
+
for (const pkg of transformed.packages) {
|
|
172
|
+
console.log(` package ${pkg.name} -> ${path.join(transformed.outBase, pkg.name)}`);
|
|
173
|
+
for (const row of pkg.rows) {
|
|
174
|
+
const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
|
|
175
|
+
console.log(` ${row.deploy}${deps}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
exports.default = async (argv, prompter, _options) => {
|
|
180
|
+
if (argv.help || argv.h) {
|
|
181
|
+
console.log(transformUsageText);
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
const granularityRaw = argv.granularity;
|
|
185
|
+
if (granularityRaw === undefined) {
|
|
186
|
+
await (0, inquirerer_1.cliExitWithError)(`--granularity is required. Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
|
|
187
|
+
}
|
|
188
|
+
if (!(0, transform_1.isExportGranularity)(granularityRaw)) {
|
|
189
|
+
await (0, inquirerer_1.cliExitWithError)(`Invalid --granularity "${granularityRaw}". Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
|
|
190
|
+
}
|
|
191
|
+
const granularity = granularityRaw;
|
|
192
|
+
const namingRaw = argv.naming ?? 'directory';
|
|
193
|
+
if (!NAMING_STYLES.includes(namingRaw)) {
|
|
194
|
+
await (0, inquirerer_1.cliExitWithError)(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
|
|
195
|
+
}
|
|
196
|
+
const naming = namingRaw;
|
|
197
|
+
const cwd = argv.cwd || process.cwd();
|
|
198
|
+
let partition;
|
|
199
|
+
if (typeof argv.partition === 'string' && argv.partition) {
|
|
200
|
+
try {
|
|
201
|
+
partition = (0, transform_1.parsePartitionConfig)(path.resolve(cwd, argv.partition));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
await (0, inquirerer_1.cliExitWithError)(err instanceof Error ? err.message : String(err));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const out = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : undefined;
|
|
208
|
+
const write = Boolean(argv.write);
|
|
209
|
+
const check = Boolean(argv.check);
|
|
210
|
+
const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
|
|
211
|
+
const pkg = new core_1.PgpmPackage(path.resolve(cwd));
|
|
212
|
+
let modulePaths;
|
|
213
|
+
if (pkg.isInModule()) {
|
|
214
|
+
modulePaths = [pkg.modulePath];
|
|
215
|
+
}
|
|
216
|
+
else if (pkg.workspacePath) {
|
|
217
|
+
const moduleMap = pkg.getModuleMap();
|
|
218
|
+
modulePaths = Object.values(moduleMap).map(mod => path.resolve(pkg.workspacePath, mod.path));
|
|
219
|
+
if (modulePaths.length === 0) {
|
|
220
|
+
await (0, inquirerer_1.cliExitWithError)('No modules found in workspace.');
|
|
221
|
+
}
|
|
222
|
+
if (out && modulePaths.length > 1) {
|
|
223
|
+
await (0, inquirerer_1.cliExitWithError)('--out is not supported when transforming a multi-module workspace.');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
await (0, inquirerer_1.cliExitWithError)('Not inside a pgpm module or workspace (pass --cwd <dir>).');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
for (const modulePath of modulePaths) {
|
|
231
|
+
let transformed;
|
|
232
|
+
try {
|
|
233
|
+
transformed = await transformModule(modulePath, granularity, naming, partition, out);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err instanceof transform_1.PartitionCycleError) {
|
|
237
|
+
await (0, inquirerer_1.cliExitWithError)(`Partition failed: ${err.message}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
for (const warning of transformed.warnings) {
|
|
243
|
+
console.warn(`transform: ${warning}`);
|
|
244
|
+
}
|
|
245
|
+
if (dryRun) {
|
|
246
|
+
printDryRun(transformed);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
for (const pkgRows of transformed.packages) {
|
|
250
|
+
const targetDir = path.join(transformed.outBase, pkgRows.name);
|
|
251
|
+
const guard = (0, emit_package_1.checkOverwrite)(targetDir, modulePath, write);
|
|
252
|
+
if (guard) {
|
|
253
|
+
await (0, inquirerer_1.cliExitWithError)(guard);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const sourceRequires = readControlRequires(modulePath, transformed.name);
|
|
257
|
+
for (const pkgRows of transformed.packages) {
|
|
258
|
+
const dir = (0, emit_package_1.writePackage)(transformed.outBase, pkgRows, sourceRequires);
|
|
259
|
+
log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
|
|
260
|
+
}
|
|
261
|
+
if (check) {
|
|
262
|
+
log.info('running --check: deploying original and transformed output to scratch databases...');
|
|
263
|
+
const diffs = await runCheck(transformed);
|
|
264
|
+
if (diffs.length) {
|
|
265
|
+
console.error(`--check failed: catalogs differ (${diffs.length} differences):`);
|
|
266
|
+
for (const diff of diffs)
|
|
267
|
+
console.error(` ${diff}`);
|
|
268
|
+
await (0, inquirerer_1.cliExitWithError)('Transform is not structurally lossless.');
|
|
269
|
+
}
|
|
270
|
+
log.success('--check passed: catalogs are structurally equivalent.');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
prompter.close();
|
|
274
|
+
return argv;
|
|
275
|
+
};
|
package/commands/update.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const utils_1 = require("@inquirerer/utils");
|
|
4
4
|
const logger_1 = require("@pgpmjs/logger");
|
|
5
|
-
const inquirerer_1 = require("inquirerer");
|
|
6
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const inquirerer_1 = require("inquirerer");
|
|
7
7
|
const npm_version_1 = require("../utils/npm-version");
|
|
8
8
|
const log = new logger_1.Logger('update');
|
|
9
9
|
const updateUsageText = `
|
package/commands.js
CHANGED
|
@@ -13,12 +13,14 @@ const analyze_1 = __importDefault(require("./commands/analyze"));
|
|
|
13
13
|
const cache_1 = __importDefault(require("./commands/cache"));
|
|
14
14
|
const clear_1 = __importDefault(require("./commands/clear"));
|
|
15
15
|
const deploy_1 = __importDefault(require("./commands/deploy"));
|
|
16
|
+
const diff_1 = __importDefault(require("./commands/diff"));
|
|
16
17
|
const docker_1 = __importDefault(require("./commands/docker"));
|
|
17
18
|
const doctor_1 = __importDefault(require("./commands/doctor"));
|
|
18
19
|
const dump_1 = __importDefault(require("./commands/dump"));
|
|
19
20
|
const env_1 = __importDefault(require("./commands/env"));
|
|
20
21
|
const export_1 = __importDefault(require("./commands/export"));
|
|
21
22
|
const extension_1 = __importDefault(require("./commands/extension"));
|
|
23
|
+
const import_1 = __importDefault(require("./commands/import"));
|
|
22
24
|
const init_1 = __importDefault(require("./commands/init"));
|
|
23
25
|
const install_1 = __importDefault(require("./commands/install"));
|
|
24
26
|
const kill_1 = __importDefault(require("./commands/kill"));
|
|
@@ -34,6 +36,7 @@ const slice_1 = __importDefault(require("./commands/slice"));
|
|
|
34
36
|
const sync_versions_1 = __importDefault(require("./commands/sync-versions"));
|
|
35
37
|
const tag_1 = __importDefault(require("./commands/tag"));
|
|
36
38
|
const test_packages_1 = __importDefault(require("./commands/test-packages"));
|
|
39
|
+
const transform_1 = __importDefault(require("./commands/transform"));
|
|
37
40
|
const tune_1 = __importDefault(require("./commands/tune"));
|
|
38
41
|
const update_1 = __importDefault(require("./commands/update"));
|
|
39
42
|
const upgrade_1 = __importDefault(require("./commands/upgrade"));
|
|
@@ -53,6 +56,7 @@ const ENGINE_EXEMPT_COMMANDS = new Set([
|
|
|
53
56
|
'doctor',
|
|
54
57
|
'env',
|
|
55
58
|
'extension',
|
|
59
|
+
'import',
|
|
56
60
|
'init',
|
|
57
61
|
'install',
|
|
58
62
|
'package',
|
|
@@ -85,6 +89,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
85
89
|
'admin-users': pgt(admin_users_1.default),
|
|
86
90
|
clear: pgt(clear_1.default),
|
|
87
91
|
deploy: pgt(deploy_1.default),
|
|
92
|
+
diff: pgt(diff_1.default),
|
|
88
93
|
docker: docker_1.default,
|
|
89
94
|
doctor: doctor_1.default,
|
|
90
95
|
dump: pgt(dump_1.default),
|
|
@@ -97,6 +102,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
97
102
|
plan: pgt(plan_1.default),
|
|
98
103
|
regen: regen_1.default,
|
|
99
104
|
export: pgt(export_1.default),
|
|
105
|
+
import: pgt(import_1.default),
|
|
100
106
|
package: pgt(package_1.default),
|
|
101
107
|
tag: pgt(tag_1.default),
|
|
102
108
|
kill: pgt(kill_1.default),
|
|
@@ -108,6 +114,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
108
114
|
slice: slice_1.default,
|
|
109
115
|
'sync-versions': sync_versions_1.default,
|
|
110
116
|
'test-packages': pgt(test_packages_1.default),
|
|
117
|
+
transform: pgt(transform_1.default),
|
|
111
118
|
tune: pgt(tune_1.default),
|
|
112
119
|
upgrade: pgt(upgrade_1.default),
|
|
113
120
|
up: pgt(upgrade_1.default),
|
package/esm/commands/cache.js
CHANGED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { PgpmMigrate } from '@pgpmjs/core';
|
|
2
|
+
import { deltaChangesToRows, loadDiffSideFromDisk, sqlToDiffChanges } from '@pgpmjs/diff';
|
|
3
|
+
import { Logger } from '@pgpmjs/logger';
|
|
4
|
+
import { diffChangeSets, EXPORT_GRANULARITIES, isExportGranularity, loadModule, withoutColumnOrder, writeModule } from '@pgpmjs/transform';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { cliExitWithError, extractFirst } from 'inquirerer';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { getPgPool } from 'pg-cache';
|
|
11
|
+
import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
|
|
12
|
+
import { emitModuleBundle, emitModuleSql, STDOUT_TARGET } from '../utils/module-projections';
|
|
13
|
+
import { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
|
|
14
|
+
const log = new Logger('diff');
|
|
15
|
+
const diffUsageText = `
|
|
16
|
+
Diff Command:
|
|
17
|
+
|
|
18
|
+
pgpm diff <A> <B> [OPTIONS]
|
|
19
|
+
|
|
20
|
+
Identity-keyed semantic diff between two schema sources. Each side is
|
|
21
|
+
normalized to an object set keyed by identity (kind/schema/name), so the
|
|
22
|
+
same schema authored at different granularity, naming, or partitioning
|
|
23
|
+
diffs as empty. Tables are compared column-by-column and
|
|
24
|
+
constraint-by-constraint, so table changes emit ALTER TABLE, not a rebuild.
|
|
25
|
+
|
|
26
|
+
Sides:
|
|
27
|
+
<A> / <B> may each be:
|
|
28
|
+
- a pgpm module directory (flattened in plan order)
|
|
29
|
+
- a raw .sql file
|
|
30
|
+
- a live database: a postgres:// connection string, or db:<name>
|
|
31
|
+
(uses PG* env for host/port/user; schema is read via pg_dump)
|
|
32
|
+
|
|
33
|
+
The delta is one model; every --emit-* flag is a projection of it, so they
|
|
34
|
+
compose (a single run can emit a module, a linear SQL file, and a bundle).
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--help, -h Show this help message
|
|
38
|
+
--emit-migration <dir> Write the delta as a pgpm module (deploy/revert/
|
|
39
|
+
verify per change, spec-derived paths, graph-derived
|
|
40
|
+
requires) into <dir>/<pkg>
|
|
41
|
+
--emit-module <dir> Alias of --emit-migration
|
|
42
|
+
--emit-sql <file|-> Also project the delta to a single consolidated SQL
|
|
43
|
+
file (deparsed in plan order); - writes to stdout
|
|
44
|
+
--emit-bundle <file> Also project the delta to a content-addressed
|
|
45
|
+
.bundle.tar.gz archive
|
|
46
|
+
--pkg <name> Emitted migration package name (default: diff-migration)
|
|
47
|
+
--granularity <level> Granularity for emitted changes: atomic | object |
|
|
48
|
+
consolidated (default: object)
|
|
49
|
+
--naming <style> Change path naming style: directory | flat (default: directory)
|
|
50
|
+
--json Machine-readable output
|
|
51
|
+
--verify Oracle mode: deploy A plus the emitted migration into
|
|
52
|
+
a scratch database and assert catalog equivalence
|
|
53
|
+
with B deployed fresh
|
|
54
|
+
--cwd <directory> Working directory (default: current directory)
|
|
55
|
+
|
|
56
|
+
Exit status: 0 when the sides are identical or the diff succeeds; non-zero on
|
|
57
|
+
error or when --verify finds the migrated catalog differs from B.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
pgpm diff ./v1-module ./v2-module
|
|
61
|
+
pgpm diff ./v1-module ./schema-v2.sql --json
|
|
62
|
+
pgpm diff db:app_v1 db:app_v2 --emit-migration ./out --verify
|
|
63
|
+
`;
|
|
64
|
+
const NAMING_STYLES = ['directory', 'flat'];
|
|
65
|
+
/** pg_dump a side's schema: `db:<name>` uses PG* env; DSNs pass through. */
|
|
66
|
+
const dumpDatabase = async (spec) => {
|
|
67
|
+
const args = ['--schema-only', '--no-owner'];
|
|
68
|
+
let env = process.env;
|
|
69
|
+
if (spec.startsWith('db:')) {
|
|
70
|
+
const config = getPgEnvOptions({ database: spec.slice(3) });
|
|
71
|
+
env = getSpawnEnvWithPg(config);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
args.push('--dbname', spec);
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const child = spawn('pg_dump', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
78
|
+
let out = '';
|
|
79
|
+
let err = '';
|
|
80
|
+
child.stdout.on('data', chunk => { out += chunk; });
|
|
81
|
+
child.stderr.on('data', chunk => { err += chunk; });
|
|
82
|
+
child.on('error', (e) => {
|
|
83
|
+
if (e.code === 'ENOENT') {
|
|
84
|
+
reject(new Error('pg_dump not found; ensure PostgreSQL client tools are installed and in PATH'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
reject(e);
|
|
88
|
+
});
|
|
89
|
+
child.on('close', code => {
|
|
90
|
+
if (code === 0)
|
|
91
|
+
resolve(out);
|
|
92
|
+
else
|
|
93
|
+
reject(new Error(`pg_dump exited with code ${code}: ${err.trim()}`));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
/** Resolve one side spec into diff input changes. */
|
|
98
|
+
const loadSide = async (spec, cwd) => {
|
|
99
|
+
if (resolveDiffSideKindSafe(spec) === 'database') {
|
|
100
|
+
const label = spec.startsWith('db:') ? spec.slice(3) : spec;
|
|
101
|
+
return {
|
|
102
|
+
kind: 'database',
|
|
103
|
+
label,
|
|
104
|
+
changes: sqlToDiffChanges(await dumpDatabase(spec), label),
|
|
105
|
+
warnings: []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return loadDiffSideFromDisk(path.resolve(cwd, spec));
|
|
109
|
+
};
|
|
110
|
+
/** `resolveDiffSideKind` without throwing on relative on-disk paths. */
|
|
111
|
+
const resolveDiffSideKindSafe = (spec) => /^postgres(ql)?:\/\//.test(spec) || spec.startsWith('db:') ? 'database' : 'disk';
|
|
112
|
+
/** Apply one diff side into a scratch database (module deploy or raw SQL). */
|
|
113
|
+
const applySide = async (config, side, spec, cwd) => {
|
|
114
|
+
if (side.kind === 'module') {
|
|
115
|
+
const client = new PgpmMigrate(config);
|
|
116
|
+
const result = await client.deploy({ modulePath: path.resolve(cwd, spec) });
|
|
117
|
+
if (result.failed) {
|
|
118
|
+
throw new Error(`deploy failed at change ${result.failed} (module ${spec})`);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const pool = getPgPool(config);
|
|
123
|
+
const sql = side.changes.map(c => c.deploy).filter(Boolean).join('\n\n');
|
|
124
|
+
if (sql.trim())
|
|
125
|
+
await pool.query(sql);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Oracle mode: deploy A into a scratch database, deploy the emitted migration
|
|
129
|
+
* module on top (when there is one), and assert catalog equivalence with B
|
|
130
|
+
* deployed fresh into a second scratch database. Column order is physical — a
|
|
131
|
+
* drop+add migration cannot reproduce a fresh deploy's ordinals — so
|
|
132
|
+
* equivalence is checked order-insensitively via `withoutColumnOrder`.
|
|
133
|
+
*/
|
|
134
|
+
const runVerify = async (sideA, sideB, specA, specB, migrationDir, cwd) => {
|
|
135
|
+
const config = getPgEnvOptions();
|
|
136
|
+
const stamp = Date.now();
|
|
137
|
+
const names = [`pgpm_diff_verify_a_${stamp}`, `pgpm_diff_verify_b_${stamp}`];
|
|
138
|
+
return withScratchDatabases(config, names, async ([cfgMigrated, cfgTarget]) => {
|
|
139
|
+
await applySide(cfgMigrated, sideA, specA, cwd);
|
|
140
|
+
if (migrationDir) {
|
|
141
|
+
const result = await new PgpmMigrate(cfgMigrated).deploy({ modulePath: migrationDir });
|
|
142
|
+
if (result.failed) {
|
|
143
|
+
throw new Error(`migration deploy failed at change ${result.failed}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await applySide(cfgTarget, sideB, specB, cwd);
|
|
147
|
+
return catalogDifferences(cfgMigrated, cfgTarget, withoutColumnOrder);
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const printSummary = (result, labelA, labelB) => {
|
|
151
|
+
if (result.identical) {
|
|
152
|
+
console.log(`No differences: ${labelA} and ${labelB} describe the same objects.`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const byDelta = (delta) => result.objects.filter(o => o.delta === delta);
|
|
156
|
+
const section = (title, objects) => {
|
|
157
|
+
if (objects.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
console.log(`${title} (${objects.length}):`);
|
|
160
|
+
for (const obj of objects) {
|
|
161
|
+
const detail = [];
|
|
162
|
+
if (obj.columnsAdded?.length)
|
|
163
|
+
detail.push(`+cols: ${obj.columnsAdded.join(', ')}`);
|
|
164
|
+
if (obj.columnsRemoved?.length)
|
|
165
|
+
detail.push(`-cols: ${obj.columnsRemoved.join(', ')}`);
|
|
166
|
+
if (obj.columnsModified?.length)
|
|
167
|
+
detail.push(`~cols: ${obj.columnsModified.join(', ')}`);
|
|
168
|
+
console.log(` ${obj.identity.kind} ${obj.path}${detail.length ? ` (${detail.join('; ')})` : ''}`);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
section('Added', byDelta('added'));
|
|
172
|
+
section('Removed', byDelta('removed'));
|
|
173
|
+
section('Changed', byDelta('modified'));
|
|
174
|
+
console.log(`${result.changes.length} migration change(s) derivable.`);
|
|
175
|
+
};
|
|
176
|
+
export default async (argv, prompter, _options) => {
|
|
177
|
+
if (argv.help || argv.h) {
|
|
178
|
+
console.log(diffUsageText);
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
const { first: specA, newArgv } = extractFirst(argv);
|
|
182
|
+
const { first: specB, newArgv: restArgv } = extractFirst(newArgv);
|
|
183
|
+
argv = restArgv;
|
|
184
|
+
if (!specA || !specB) {
|
|
185
|
+
await cliExitWithError('pgpm diff requires two sides: pgpm diff <A> <B> (module dir, .sql file, db:<name>, or connection string).');
|
|
186
|
+
}
|
|
187
|
+
const granularityRaw = argv.granularity ?? 'object';
|
|
188
|
+
if (!isExportGranularity(granularityRaw)) {
|
|
189
|
+
await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
|
|
190
|
+
}
|
|
191
|
+
const granularity = granularityRaw;
|
|
192
|
+
const namingRaw = argv.naming ?? 'directory';
|
|
193
|
+
if (!NAMING_STYLES.includes(namingRaw)) {
|
|
194
|
+
await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
|
|
195
|
+
}
|
|
196
|
+
const naming = namingRaw;
|
|
197
|
+
const cwd = argv.cwd || process.cwd();
|
|
198
|
+
const json = Boolean(argv.json);
|
|
199
|
+
const verify = Boolean(argv.verify);
|
|
200
|
+
const emitModuleRaw = argv['emit-migration'] ?? argv.emitMigration ?? argv['emit-module'] ?? argv.emitModule;
|
|
201
|
+
const emitModuleDir = typeof emitModuleRaw === 'string' && emitModuleRaw
|
|
202
|
+
? path.resolve(cwd, emitModuleRaw)
|
|
203
|
+
: undefined;
|
|
204
|
+
const emitSqlRaw = argv['emit-sql'] ?? argv.emitSql;
|
|
205
|
+
const emitSql = typeof emitSqlRaw === 'string' && emitSqlRaw
|
|
206
|
+
? (emitSqlRaw === STDOUT_TARGET ? STDOUT_TARGET : path.resolve(cwd, emitSqlRaw))
|
|
207
|
+
: undefined;
|
|
208
|
+
const emitBundleRaw = argv['emit-bundle'] ?? argv.emitBundle;
|
|
209
|
+
const emitBundle = typeof emitBundleRaw === 'string' && emitBundleRaw
|
|
210
|
+
? path.resolve(cwd, emitBundleRaw)
|
|
211
|
+
: undefined;
|
|
212
|
+
const sqlToStdout = emitSql === STDOUT_TARGET;
|
|
213
|
+
const pkgName = argv.pkg || 'diff-migration';
|
|
214
|
+
await loadModule();
|
|
215
|
+
let sideA;
|
|
216
|
+
let sideB;
|
|
217
|
+
try {
|
|
218
|
+
sideA = await loadSide(specA, cwd);
|
|
219
|
+
sideB = await loadSide(specB, cwd);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
await cliExitWithError(err instanceof Error ? err.message : String(err));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const result = diffChangeSets(sideA.changes, sideB.changes, { granularity, style: naming });
|
|
226
|
+
const warnings = [
|
|
227
|
+
...sideA.warnings.map(w => `${sideA.label}: ${w}`),
|
|
228
|
+
...sideB.warnings.map(w => `${sideB.label}: ${w}`),
|
|
229
|
+
...result.warnings
|
|
230
|
+
];
|
|
231
|
+
if (json) {
|
|
232
|
+
console.log(JSON.stringify({
|
|
233
|
+
identical: result.identical,
|
|
234
|
+
objects: result.objects,
|
|
235
|
+
changes: result.changes.map(c => ({
|
|
236
|
+
name: c.name,
|
|
237
|
+
dependencies: c.dependencies,
|
|
238
|
+
deploy: c.deploy,
|
|
239
|
+
revert: c.revert,
|
|
240
|
+
verify: c.verify
|
|
241
|
+
})),
|
|
242
|
+
warnings
|
|
243
|
+
}, null, 2));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
for (const warning of warnings)
|
|
247
|
+
console.warn(`diff: ${warning}`);
|
|
248
|
+
// Keep stdout clean when the SQL projection is piped there.
|
|
249
|
+
if (!sqlToStdout)
|
|
250
|
+
printSummary(result, sideA.label, sideB.label);
|
|
251
|
+
}
|
|
252
|
+
let migrationDir;
|
|
253
|
+
const needModule = Boolean(emitModuleDir || emitSql || emitBundle) || (verify && !result.identical);
|
|
254
|
+
if (needModule) {
|
|
255
|
+
const outBase = emitModuleDir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-diff-'));
|
|
256
|
+
const rows = deltaChangesToRows(result.changes);
|
|
257
|
+
if (rows.length === 0) {
|
|
258
|
+
if (!sqlToStdout)
|
|
259
|
+
log.info('no migration changes to emit (sides are identical).');
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
migrationDir = writeModule(outBase, { name: pkgName, requires: [], rows });
|
|
263
|
+
if (emitModuleDir && !sqlToStdout)
|
|
264
|
+
log.success(`wrote ${rows.length} migration change(s) to ${migrationDir}`);
|
|
265
|
+
if (emitSql) {
|
|
266
|
+
await emitModuleSql(migrationDir, emitSql);
|
|
267
|
+
if (!sqlToStdout)
|
|
268
|
+
log.success(`wrote linear SQL to ${emitSql}`);
|
|
269
|
+
}
|
|
270
|
+
if (emitBundle) {
|
|
271
|
+
await emitModuleBundle(migrationDir, emitBundle);
|
|
272
|
+
if (!sqlToStdout)
|
|
273
|
+
log.success(`wrote bundle to ${emitBundle}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (verify) {
|
|
278
|
+
log.info('running --verify: deploying A plus the migration and B into scratch databases...');
|
|
279
|
+
const diffs = await runVerify(sideA, sideB, specA, specB, migrationDir, cwd);
|
|
280
|
+
if (diffs.length) {
|
|
281
|
+
console.error(`--verify failed: catalogs differ (${diffs.length} differences):`);
|
|
282
|
+
for (const diff of diffs)
|
|
283
|
+
console.error(` ${diff}`);
|
|
284
|
+
await cliExitWithError('Migrated catalog does not match the target.');
|
|
285
|
+
}
|
|
286
|
+
log.success('--verify passed: migrated catalog is structurally equivalent to the target.');
|
|
287
|
+
}
|
|
288
|
+
prompter.close();
|
|
289
|
+
return argv;
|
|
290
|
+
};
|