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/esm/commands/dump.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Logger } from '@pgpmjs/logger';
|
|
2
|
-
import {
|
|
3
|
-
import { getPgPool } from 'pg-cache';
|
|
2
|
+
import { QuoteUtils } from '@pgsql/quotes';
|
|
4
3
|
import { spawn } from 'child_process';
|
|
5
4
|
import fs from 'fs';
|
|
6
5
|
import path from 'path';
|
|
7
|
-
import {
|
|
6
|
+
import { getPgPool } from 'pg-cache';
|
|
7
|
+
import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
|
|
8
8
|
import { getTargetDatabase } from '../utils';
|
|
9
9
|
const log = new Logger('dump');
|
|
10
10
|
const dumpUsageText = `
|
package/esm/commands/export.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PgpmPackage } from '@pgpmjs/core';
|
|
2
|
-
import { exportMigrations, exportGraphQL, GraphQLClient, graphqlRowToPostgresRow, isExportGranularity, EXPORT_GRANULARITIES } from '@pgpmjs/export';
|
|
3
2
|
import { getEnvOptions } from '@pgpmjs/env';
|
|
3
|
+
import { exportGraphQL, exportMigrations, GraphQLClient, graphqlRowToPostgresRow } from '@pgpmjs/export';
|
|
4
|
+
import { EXPORT_GRANULARITIES, isExportGranularity, parsePartitionConfig } from '@pgpmjs/transform';
|
|
4
5
|
import { getGitConfigInfo } from '@pgpmjs/types';
|
|
5
6
|
import { resolve } from 'path';
|
|
6
7
|
import { getPgPool } from 'pg-cache';
|
|
@@ -28,12 +29,16 @@ Options:
|
|
|
28
29
|
Change paths are derived from the naming spec and
|
|
29
30
|
requires from the statement graph. When omitted,
|
|
30
31
|
sql_actions rows are exported unchanged.
|
|
32
|
+
--partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
|
|
33
|
+
splitting the exported database module into multiple
|
|
34
|
+
pgpm packages with derived cross-package requires.
|
|
31
35
|
--cwd <directory> Working directory (default: current directory)
|
|
32
36
|
|
|
33
37
|
Examples:
|
|
34
38
|
pgpm export Export migrations from selected database (SQL mode)
|
|
35
39
|
pgpm export --exclude-categories security,permissions,auth,memberships
|
|
36
40
|
pgpm export --granularity consolidated
|
|
41
|
+
pgpm export --granularity object --partition partition.json
|
|
37
42
|
pgpm export --graphql-endpoint 'http://[::1]:3002/graphql' --migrate-endpoint 'http://[::1]:3000/graphql' --migrate-host db_migrate.localhost:3000
|
|
38
43
|
`;
|
|
39
44
|
export default async (argv, prompter, _options) => {
|
|
@@ -65,6 +70,18 @@ export default async (argv, prompter, _options) => {
|
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
72
|
const granularity = granularityRaw;
|
|
73
|
+
const partitionRaw = argv.partition;
|
|
74
|
+
let partition;
|
|
75
|
+
if (typeof partitionRaw === 'string' && partitionRaw) {
|
|
76
|
+
try {
|
|
77
|
+
partition = parsePartitionConfig(resolve(cwd, partitionRaw));
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
81
|
+
prompter.close();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
68
85
|
if (graphqlEndpoint) {
|
|
69
86
|
// =========================================================================
|
|
70
87
|
// GraphQL export mode
|
|
@@ -162,7 +179,8 @@ export default async (argv, prompter, _options) => {
|
|
|
162
179
|
argv,
|
|
163
180
|
username,
|
|
164
181
|
excludeCategories,
|
|
165
|
-
granularity
|
|
182
|
+
granularity,
|
|
183
|
+
partition
|
|
166
184
|
});
|
|
167
185
|
}
|
|
168
186
|
else {
|
|
@@ -283,7 +301,8 @@ export default async (argv, prompter, _options) => {
|
|
|
283
301
|
argv,
|
|
284
302
|
username,
|
|
285
303
|
excludeCategories,
|
|
286
|
-
granularity
|
|
304
|
+
granularity,
|
|
305
|
+
partition
|
|
287
306
|
});
|
|
288
307
|
}
|
|
289
308
|
prompter.close();
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { dumpCompatibilityWarnings, importDumpRows, linkTextualDeps, loadDumpSource } from '@pgpmjs/import';
|
|
2
|
+
import { Logger } from '@pgpmjs/logger';
|
|
3
|
+
import { EXPORT_GRANULARITIES, isExportGranularity, parsePartitionConfig, PartitionCycleError, partitionExportRows } from '@pgpmjs/transform';
|
|
4
|
+
import { cliExitWithError } from 'inquirerer';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { checkOverwrite, writePackage } from '../utils/emit-package';
|
|
7
|
+
const log = new Logger('import');
|
|
8
|
+
const importUsageText = `
|
|
9
|
+
Import Command:
|
|
10
|
+
|
|
11
|
+
pgpm import <dump.sql | dir-of-sql-files> --pkg <module-name> [OPTIONS]
|
|
12
|
+
|
|
13
|
+
pgpm-itize an arbitrary SQL dump (e.g. pg_dump --schema-only output): parse
|
|
14
|
+
it, classify every statement, and emit a complete deployable pgpm module —
|
|
15
|
+
module dir, pgpm.plan, and deploy/revert/verify trees with spec-derived
|
|
16
|
+
change paths, graph-derived requires, and generated revert/verify scripts.
|
|
17
|
+
|
|
18
|
+
Directory inputs concatenate their .sql files in sorted (lexicographic)
|
|
19
|
+
filename order.
|
|
20
|
+
|
|
21
|
+
Dump handling:
|
|
22
|
+
- pg_dump preamble noise (SET ..., SELECT set_config(...), psql backslash
|
|
23
|
+
commands) is skipped; COMMENT ON and GRANT statements are kept, attached
|
|
24
|
+
to their host object's change.
|
|
25
|
+
- CREATE EXTENSION statements become .control requires (pgpm owns
|
|
26
|
+
extension creation at deploy time).
|
|
27
|
+
- COPY ... FROM stdin blocks and INSERTs are skipped with a warning unless
|
|
28
|
+
--with-data is passed, which emits them as seed fixture changes (COPY
|
|
29
|
+
data is converted to INSERTs).
|
|
30
|
+
- Statements that classify to no object land in a misc/statements change
|
|
31
|
+
with a warning — never dropped silently.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--help, -h Show this help message
|
|
35
|
+
--pkg <name> Module name for the generated package (required)
|
|
36
|
+
--granularity <level> Granularity dial: atomic | object | consolidated
|
|
37
|
+
(default: object)
|
|
38
|
+
--naming <style> Change path naming style: directory | flat (default: directory)
|
|
39
|
+
--out <dir> Output base directory (default: current directory);
|
|
40
|
+
the module is written to <out>/<pkg>
|
|
41
|
+
--with-data Import COPY/INSERT data as seed fixture changes
|
|
42
|
+
--partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
|
|
43
|
+
splitting the import into multiple pgpm packages with
|
|
44
|
+
derived cross-package requires
|
|
45
|
+
--write Allow overwriting an existing module directory
|
|
46
|
+
--cwd <directory> Working directory (default: current directory)
|
|
47
|
+
--dry-run Print the resulting plan/paths without writing
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
pgpm import dump.sql --pkg my-app
|
|
51
|
+
pgpm import dump.sql --pkg my-app --granularity consolidated --naming flat
|
|
52
|
+
pgpm import ./sql-files --pkg my-app --out ./packages --with-data
|
|
53
|
+
pgpm import dump.sql --pkg my-app --partition partition.json
|
|
54
|
+
`;
|
|
55
|
+
const NAMING_STYLES = ['directory', 'flat'];
|
|
56
|
+
export default async (argv, prompter, _options) => {
|
|
57
|
+
if (argv.help || argv.h) {
|
|
58
|
+
console.log(importUsageText);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
const cwd = argv.cwd || process.cwd();
|
|
62
|
+
const input = argv._?.filter(Boolean)[0];
|
|
63
|
+
if (!input) {
|
|
64
|
+
await cliExitWithError('Missing input: pgpm import <dump.sql | dir-of-sql-files> --pkg <name>');
|
|
65
|
+
}
|
|
66
|
+
const pkgName = argv.pkg;
|
|
67
|
+
if (!pkgName || typeof pkgName !== 'string') {
|
|
68
|
+
await cliExitWithError('--pkg <module-name> is required.');
|
|
69
|
+
}
|
|
70
|
+
const granularityRaw = argv.granularity ?? 'object';
|
|
71
|
+
if (!isExportGranularity(granularityRaw)) {
|
|
72
|
+
await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
|
|
73
|
+
}
|
|
74
|
+
const granularity = granularityRaw;
|
|
75
|
+
const namingRaw = argv.naming ?? 'directory';
|
|
76
|
+
if (!NAMING_STYLES.includes(namingRaw)) {
|
|
77
|
+
await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
|
|
78
|
+
}
|
|
79
|
+
const naming = namingRaw;
|
|
80
|
+
let partition;
|
|
81
|
+
if (typeof argv.partition === 'string' && argv.partition) {
|
|
82
|
+
try {
|
|
83
|
+
partition = parsePartitionConfig(path.resolve(cwd, argv.partition));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
await cliExitWithError(err instanceof Error ? err.message : String(err));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const outBase = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : path.resolve(cwd);
|
|
90
|
+
const withData = Boolean(argv['with-data'] ?? argv.withData);
|
|
91
|
+
const write = Boolean(argv.write);
|
|
92
|
+
const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
|
|
93
|
+
let source;
|
|
94
|
+
try {
|
|
95
|
+
source = loadDumpSource(path.resolve(cwd, input));
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
await cliExitWithError(err instanceof Error ? err.message : String(err));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (source.files.length > 1) {
|
|
102
|
+
log.info(`concatenated ${source.files.length} .sql files in sorted order`);
|
|
103
|
+
}
|
|
104
|
+
for (const warning of dumpCompatibilityWarnings(source)) {
|
|
105
|
+
console.warn(`\nWARNING: ${warning}\n`);
|
|
106
|
+
}
|
|
107
|
+
const result = await importDumpRows(source, { granularity, naming, withData });
|
|
108
|
+
let packages;
|
|
109
|
+
if (partition) {
|
|
110
|
+
try {
|
|
111
|
+
const partitioned = await partitionExportRows(result.rows, partition);
|
|
112
|
+
result.warnings.push(...partitioned.warnings.map(w => `partition: ${w}`));
|
|
113
|
+
packages = partitioned.packages.map(pkg => ({
|
|
114
|
+
name: pkg.name,
|
|
115
|
+
requires: [...result.controlRequires, ...pkg.requires],
|
|
116
|
+
rows: linkTextualDeps(pkg.rows)
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err instanceof PartitionCycleError) {
|
|
121
|
+
await cliExitWithError(`Partition failed: ${err.message}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
packages = [{ name: pkgName, requires: result.controlRequires, rows: result.rows }];
|
|
129
|
+
}
|
|
130
|
+
for (const warning of result.warnings) {
|
|
131
|
+
console.warn(`import: ${warning}`);
|
|
132
|
+
}
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
for (const pkg of packages) {
|
|
135
|
+
console.log(`package ${pkg.name} -> ${path.join(outBase, pkg.name)}`);
|
|
136
|
+
if (pkg.requires.length) {
|
|
137
|
+
console.log(` requires: ${pkg.requires.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
for (const row of pkg.rows) {
|
|
140
|
+
const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
|
|
141
|
+
console.log(` ${row.deploy}${deps}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
for (const pkg of packages) {
|
|
147
|
+
const guard = checkOverwrite(path.join(outBase, pkg.name), source.files[0], write);
|
|
148
|
+
if (guard) {
|
|
149
|
+
await cliExitWithError(guard);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const pkg of packages) {
|
|
153
|
+
const dir = writePackage(outBase, pkg);
|
|
154
|
+
log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const { summary } = result;
|
|
158
|
+
log.success(`import: ${summary.statements} statement(s) -> ${summary.changes} change(s), ` +
|
|
159
|
+
`${summary.skippedPreamble} preamble skipped, ${summary.skippedData} data skipped, ` +
|
|
160
|
+
`${summary.misc} in misc, ${result.warnings.length} warning(s)`);
|
|
161
|
+
prompter.close();
|
|
162
|
+
return argv;
|
|
163
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { DEFAULT_TEMPLATE_REPO, PgpmPackage, TEMPLATE_REPOS } from '@pgpmjs/core';
|
|
1
2
|
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
|
-
import { DEFAULT_TEMPLATE_REPO, PgpmPackage, TEMPLATE_REPOS } from '@pgpmjs/core';
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the template repo for an `init` invocation from its flags.
|
|
6
6
|
*
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
1
|
import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, inspectTemplate, PgpmPackage, resolveBoilerplateBaseDir, scaffoldTemplate, scanBoilerplates, SkillInstaller, sluggify, } from '@pgpmjs/core';
|
|
5
2
|
import { resolveWorkspaceByType } from '@pgpmjs/env';
|
|
6
3
|
import { errors } from '@pgpmjs/types';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
7
6
|
import { registerDefaultResolver } from 'inquirerer';
|
|
7
|
+
import path from 'path';
|
|
8
8
|
import { isNoTtyRequested } from '../../utils';
|
|
9
9
|
import { persistBoilerplateSource, readBoilerplateSource, resolveInitTemplateRepo, } from './boilerplate';
|
|
10
10
|
const DEFAULT_MOTD = `
|
package/esm/commands/package.js
CHANGED
|
@@ -116,7 +116,6 @@ export default async (argv, prompter, _options) => {
|
|
|
116
116
|
const project = new PgpmPackage(cwd);
|
|
117
117
|
project.ensureModule();
|
|
118
118
|
const info = project.getModuleInfo();
|
|
119
|
-
info.version;
|
|
120
119
|
await writePackage({
|
|
121
120
|
version: info.version,
|
|
122
121
|
extension: true,
|
package/esm/commands/slice.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PgpmPackage } from '@pgpmjs/core';
|
|
2
2
|
import { generateDryRunReport, slicePlan, writeSliceResult } from '@pgpmjs/slice';
|
|
3
3
|
import { getGitConfigInfo } from '@pgpmjs/types';
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
5
|
import { resolve } from 'path';
|
|
5
|
-
import { readFileSync, existsSync } from 'fs';
|
|
6
6
|
const sliceUsageText = `
|
|
7
7
|
Slice Command:
|
|
8
8
|
|
|
@@ -2,8 +2,8 @@ import { PgpmPackage } from '@pgpmjs/core';
|
|
|
2
2
|
import { getEnvOptions } from '@pgpmjs/env';
|
|
3
3
|
import { Logger } from '@pgpmjs/logger';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { getPgEnvOptions } from 'pg-env';
|
|
6
5
|
import { getPgPool } from 'pg-cache';
|
|
6
|
+
import { getPgEnvOptions } from 'pg-env';
|
|
7
7
|
const log = new Logger('test-packages');
|
|
8
8
|
// ANSI color codes
|
|
9
9
|
const RED = '\x1b[0;31m';
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { PgpmMigrate, PgpmPackage } from '@pgpmjs/core';
|
|
2
|
+
import { Logger } from '@pgpmjs/logger';
|
|
3
|
+
import { EXPORT_GRANULARITIES, isExportGranularity, loadModuleSource, parsePartitionConfig, PartitionCycleError, partitionExportRows, restructureExportRows } from '@pgpmjs/transform';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { cliExitWithError } from 'inquirerer';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { getPgEnvOptions } from 'pg-env';
|
|
8
|
+
import { checkOverwrite, writePackage } from '../utils/emit-package';
|
|
9
|
+
import { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
|
|
10
|
+
export { checkOverwrite } from '../utils/emit-package';
|
|
11
|
+
const log = new Logger('transform');
|
|
12
|
+
const transformUsageText = `
|
|
13
|
+
Transform Command:
|
|
14
|
+
|
|
15
|
+
pgpm transform --granularity <atomic|object|consolidated> [OPTIONS]
|
|
16
|
+
|
|
17
|
+
Re-dial an existing pgpm module (or every module in a workspace) through the
|
|
18
|
+
dials pipeline: flatten the deploy scripts in plan order, re-project them at
|
|
19
|
+
the requested granularity with spec-derived change paths, graph-derived
|
|
20
|
+
requires, and generated revert/verify scripts.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--help, -h Show this help message
|
|
24
|
+
--granularity <level> Target granularity: atomic | object | consolidated (required)
|
|
25
|
+
--partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
|
|
26
|
+
splitting the module into multiple pgpm packages with
|
|
27
|
+
derived cross-package requires.
|
|
28
|
+
--naming <style> Change path naming style: directory | flat (default: directory)
|
|
29
|
+
--cwd <directory> Module or workspace directory (default: current directory)
|
|
30
|
+
--out <dir> Output directory (default: sibling <module>-<granularity>)
|
|
31
|
+
--write Allow writing over an existing/module directory
|
|
32
|
+
--check Deploy original and transformed output into scratch
|
|
33
|
+
databases and assert the catalogs are equivalent
|
|
34
|
+
--dry-run Print the resulting plan/paths without writing
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
pgpm transform --granularity object
|
|
38
|
+
pgpm transform --granularity atomic --naming flat --out ./out
|
|
39
|
+
pgpm transform --granularity object --partition partition.json --check
|
|
40
|
+
`;
|
|
41
|
+
const NAMING_STYLES = ['directory', 'flat'];
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the base directory package dir(s) are created in. Without --out,
|
|
44
|
+
* packages are written as siblings of the source module — a plain transform
|
|
45
|
+
* of `my-mod` at granularity `object` lands in `../my-mod-object`.
|
|
46
|
+
*/
|
|
47
|
+
export const resolveOutBase = (modulePath, out) => {
|
|
48
|
+
if (out)
|
|
49
|
+
return path.resolve(out);
|
|
50
|
+
return path.dirname(modulePath);
|
|
51
|
+
};
|
|
52
|
+
/** Order partition packages so prerequisites come before dependents. */
|
|
53
|
+
export const orderPackages = (packages) => {
|
|
54
|
+
const byName = new Map(packages.map(pkg => [pkg.name, pkg]));
|
|
55
|
+
const ordered = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const visit = (pkg) => {
|
|
58
|
+
if (seen.has(pkg.name))
|
|
59
|
+
return;
|
|
60
|
+
seen.add(pkg.name);
|
|
61
|
+
for (const req of pkg.requires) {
|
|
62
|
+
const dep = byName.get(req);
|
|
63
|
+
if (dep)
|
|
64
|
+
visit(dep);
|
|
65
|
+
}
|
|
66
|
+
ordered.push(pkg);
|
|
67
|
+
};
|
|
68
|
+
for (const pkg of packages)
|
|
69
|
+
visit(pkg);
|
|
70
|
+
return ordered;
|
|
71
|
+
};
|
|
72
|
+
const readControlRequires = (modulePath, name) => {
|
|
73
|
+
const controlPath = path.join(modulePath, `${name}.control`);
|
|
74
|
+
if (!fs.existsSync(controlPath))
|
|
75
|
+
return [];
|
|
76
|
+
const match = fs.readFileSync(controlPath, 'utf-8').match(/^requires\s*=\s*'([^']*)'\s*$/m);
|
|
77
|
+
if (!match)
|
|
78
|
+
return [];
|
|
79
|
+
return match[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
80
|
+
};
|
|
81
|
+
const deployModules = async (config, modulePaths) => {
|
|
82
|
+
const client = new PgpmMigrate(config);
|
|
83
|
+
for (const modulePath of modulePaths) {
|
|
84
|
+
const result = await client.deploy({ modulePath });
|
|
85
|
+
if (result.failed) {
|
|
86
|
+
throw new Error(`deploy failed at change ${result.failed} (module ${modulePath})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Prove the transform is structurally lossless: deploy the original module and
|
|
92
|
+
* the transformed package(s) into two scratch databases and compare catalogs.
|
|
93
|
+
*/
|
|
94
|
+
const runCheck = async (transformed) => {
|
|
95
|
+
const config = getPgEnvOptions();
|
|
96
|
+
const stamp = Date.now();
|
|
97
|
+
const names = [`pgpm_transform_check_a_${stamp}`, `pgpm_transform_check_b_${stamp}`];
|
|
98
|
+
return withScratchDatabases(config, names, async ([cfgOriginal, cfgTransformed]) => {
|
|
99
|
+
await deployModules(cfgOriginal, [transformed.modulePath]);
|
|
100
|
+
const orderedDirs = orderPackages(transformed.packages).map(pkg => path.join(transformed.outBase, pkg.name));
|
|
101
|
+
await deployModules(cfgTransformed, orderedDirs);
|
|
102
|
+
return catalogDifferences(cfgOriginal, cfgTransformed);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const transformModule = async (modulePath, granularity, naming, partition, out) => {
|
|
106
|
+
const source = loadModuleSource(modulePath);
|
|
107
|
+
const warnings = [...source.warnings];
|
|
108
|
+
const rows = source.changes.map(change => ({
|
|
109
|
+
name: change.name,
|
|
110
|
+
deploy: change.name,
|
|
111
|
+
deps: change.dependencies,
|
|
112
|
+
content: change.deploy
|
|
113
|
+
}));
|
|
114
|
+
const restructured = await restructureExportRows(rows, granularity, { naming });
|
|
115
|
+
warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
|
|
116
|
+
const outBase = resolveOutBase(modulePath, out);
|
|
117
|
+
let packages;
|
|
118
|
+
if (partition) {
|
|
119
|
+
const result = await partitionExportRows(restructured.rows, partition);
|
|
120
|
+
warnings.push(...result.warnings.map(w => `partition: ${w}`));
|
|
121
|
+
packages = result.packages;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Distinct package name so the output can live beside the source module
|
|
125
|
+
// in the same workspace without colliding.
|
|
126
|
+
packages = [{ name: `${source.name}-${granularity}`, requires: [], rows: restructured.rows }];
|
|
127
|
+
}
|
|
128
|
+
return { modulePath, name: source.name, outBase, packages, warnings };
|
|
129
|
+
};
|
|
130
|
+
const printDryRun = (transformed) => {
|
|
131
|
+
console.log(`module ${transformed.name} (${transformed.modulePath})`);
|
|
132
|
+
for (const pkg of transformed.packages) {
|
|
133
|
+
console.log(` package ${pkg.name} -> ${path.join(transformed.outBase, pkg.name)}`);
|
|
134
|
+
for (const row of pkg.rows) {
|
|
135
|
+
const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
|
|
136
|
+
console.log(` ${row.deploy}${deps}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
export default async (argv, prompter, _options) => {
|
|
141
|
+
if (argv.help || argv.h) {
|
|
142
|
+
console.log(transformUsageText);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
const granularityRaw = argv.granularity;
|
|
146
|
+
if (granularityRaw === undefined) {
|
|
147
|
+
await cliExitWithError(`--granularity is required. Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
|
|
148
|
+
}
|
|
149
|
+
if (!isExportGranularity(granularityRaw)) {
|
|
150
|
+
await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
|
|
151
|
+
}
|
|
152
|
+
const granularity = granularityRaw;
|
|
153
|
+
const namingRaw = argv.naming ?? 'directory';
|
|
154
|
+
if (!NAMING_STYLES.includes(namingRaw)) {
|
|
155
|
+
await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
|
|
156
|
+
}
|
|
157
|
+
const naming = namingRaw;
|
|
158
|
+
const cwd = argv.cwd || process.cwd();
|
|
159
|
+
let partition;
|
|
160
|
+
if (typeof argv.partition === 'string' && argv.partition) {
|
|
161
|
+
try {
|
|
162
|
+
partition = parsePartitionConfig(path.resolve(cwd, argv.partition));
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
await cliExitWithError(err instanceof Error ? err.message : String(err));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const out = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : undefined;
|
|
169
|
+
const write = Boolean(argv.write);
|
|
170
|
+
const check = Boolean(argv.check);
|
|
171
|
+
const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
|
|
172
|
+
const pkg = new PgpmPackage(path.resolve(cwd));
|
|
173
|
+
let modulePaths;
|
|
174
|
+
if (pkg.isInModule()) {
|
|
175
|
+
modulePaths = [pkg.modulePath];
|
|
176
|
+
}
|
|
177
|
+
else if (pkg.workspacePath) {
|
|
178
|
+
const moduleMap = pkg.getModuleMap();
|
|
179
|
+
modulePaths = Object.values(moduleMap).map(mod => path.resolve(pkg.workspacePath, mod.path));
|
|
180
|
+
if (modulePaths.length === 0) {
|
|
181
|
+
await cliExitWithError('No modules found in workspace.');
|
|
182
|
+
}
|
|
183
|
+
if (out && modulePaths.length > 1) {
|
|
184
|
+
await cliExitWithError('--out is not supported when transforming a multi-module workspace.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
await cliExitWithError('Not inside a pgpm module or workspace (pass --cwd <dir>).');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const modulePath of modulePaths) {
|
|
192
|
+
let transformed;
|
|
193
|
+
try {
|
|
194
|
+
transformed = await transformModule(modulePath, granularity, naming, partition, out);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err instanceof PartitionCycleError) {
|
|
198
|
+
await cliExitWithError(`Partition failed: ${err.message}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
for (const warning of transformed.warnings) {
|
|
204
|
+
console.warn(`transform: ${warning}`);
|
|
205
|
+
}
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
printDryRun(transformed);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const pkgRows of transformed.packages) {
|
|
211
|
+
const targetDir = path.join(transformed.outBase, pkgRows.name);
|
|
212
|
+
const guard = checkOverwrite(targetDir, modulePath, write);
|
|
213
|
+
if (guard) {
|
|
214
|
+
await cliExitWithError(guard);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const sourceRequires = readControlRequires(modulePath, transformed.name);
|
|
218
|
+
for (const pkgRows of transformed.packages) {
|
|
219
|
+
const dir = writePackage(transformed.outBase, pkgRows, sourceRequires);
|
|
220
|
+
log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
|
|
221
|
+
}
|
|
222
|
+
if (check) {
|
|
223
|
+
log.info('running --check: deploying original and transformed output to scratch databases...');
|
|
224
|
+
const diffs = await runCheck(transformed);
|
|
225
|
+
if (diffs.length) {
|
|
226
|
+
console.error(`--check failed: catalogs differ (${diffs.length} differences):`);
|
|
227
|
+
for (const diff of diffs)
|
|
228
|
+
console.error(` ${diff}`);
|
|
229
|
+
await cliExitWithError('Transform is not structurally lossless.');
|
|
230
|
+
}
|
|
231
|
+
log.success('--check passed: catalogs are structurally equivalent.');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
prompter.close();
|
|
235
|
+
return argv;
|
|
236
|
+
};
|
package/esm/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { suppressUpdateCheck } from '@inquirerer/utils';
|
|
2
2
|
import { Logger } from '@pgpmjs/logger';
|
|
3
|
-
import { cliExitWithError, getPackageJson } from 'inquirerer';
|
|
4
3
|
import { spawn } from 'child_process';
|
|
4
|
+
import { cliExitWithError, getPackageJson } from 'inquirerer';
|
|
5
5
|
import { fetchLatestVersion } from '../utils/npm-version';
|
|
6
6
|
const log = new Logger('update');
|
|
7
7
|
const updateUsageText = `
|
package/esm/commands.js
CHANGED
|
@@ -7,12 +7,14 @@ import analyze from './commands/analyze';
|
|
|
7
7
|
import cache from './commands/cache';
|
|
8
8
|
import clear from './commands/clear';
|
|
9
9
|
import deploy from './commands/deploy';
|
|
10
|
+
import diff from './commands/diff';
|
|
10
11
|
import docker from './commands/docker';
|
|
11
12
|
import doctor from './commands/doctor';
|
|
12
13
|
import dump from './commands/dump';
|
|
13
14
|
import env from './commands/env';
|
|
14
15
|
import _export from './commands/export';
|
|
15
16
|
import extension from './commands/extension';
|
|
17
|
+
import _import from './commands/import';
|
|
16
18
|
import init from './commands/init';
|
|
17
19
|
import install from './commands/install';
|
|
18
20
|
import kill from './commands/kill';
|
|
@@ -28,6 +30,7 @@ import slice from './commands/slice';
|
|
|
28
30
|
import syncVersions from './commands/sync-versions';
|
|
29
31
|
import tag from './commands/tag';
|
|
30
32
|
import testPackages from './commands/test-packages';
|
|
33
|
+
import transform from './commands/transform';
|
|
31
34
|
import tune from './commands/tune';
|
|
32
35
|
import updateCmd from './commands/update';
|
|
33
36
|
import upgrade from './commands/upgrade';
|
|
@@ -47,6 +50,7 @@ const ENGINE_EXEMPT_COMMANDS = new Set([
|
|
|
47
50
|
'doctor',
|
|
48
51
|
'env',
|
|
49
52
|
'extension',
|
|
53
|
+
'import',
|
|
50
54
|
'init',
|
|
51
55
|
'install',
|
|
52
56
|
'package',
|
|
@@ -79,6 +83,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
79
83
|
'admin-users': pgt(adminUsers),
|
|
80
84
|
clear: pgt(clear),
|
|
81
85
|
deploy: pgt(deploy),
|
|
86
|
+
diff: pgt(diff),
|
|
82
87
|
docker,
|
|
83
88
|
doctor,
|
|
84
89
|
dump: pgt(dump),
|
|
@@ -91,6 +96,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
91
96
|
plan: pgt(plan),
|
|
92
97
|
regen,
|
|
93
98
|
export: pgt(_export),
|
|
99
|
+
import: pgt(_import),
|
|
94
100
|
package: pgt(_package),
|
|
95
101
|
tag: pgt(tag),
|
|
96
102
|
kill: pgt(kill),
|
|
@@ -102,6 +108,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
102
108
|
slice,
|
|
103
109
|
'sync-versions': syncVersions,
|
|
104
110
|
'test-packages': pgt(testPackages),
|
|
111
|
+
transform: pgt(transform),
|
|
105
112
|
tune: pgt(tune),
|
|
106
113
|
upgrade: pgt(upgrade),
|
|
107
114
|
up: pgt(upgrade),
|
package/esm/utils/display.js
CHANGED
|
@@ -12,6 +12,9 @@ export const usageText = `
|
|
|
12
12
|
extension Manage module dependencies
|
|
13
13
|
plan Generate module deployment plans
|
|
14
14
|
regen Generate revert/verify scripts from deploy scripts
|
|
15
|
+
transform Re-dial a module through the dials pipeline (granularity/naming/partition)
|
|
16
|
+
import pgpm-itize an arbitrary SQL dump into a deployable module
|
|
17
|
+
diff Identity-keyed semantic diff between two schema sources (+ migration generation)
|
|
15
18
|
package Package module for distribution
|
|
16
19
|
materialize Transpile an apply proxy into a plain, committed module
|
|
17
20
|
sync-versions Sync .control/Makefile/sql metadata to package.json versions
|
package/esm/utils/driver.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { PGPM_DRIVER_EXPORT, } from '@pgpmjs/types';
|
|
2
1
|
import { createRequire } from 'node:module';
|
|
3
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { PGPM_DRIVER_EXPORT, } from '@pgpmjs/types';
|
|
4
4
|
/** Package name of the built-in PGlite driver plugin (the `--pglite` alias). */
|
|
5
5
|
export const PGLITE_DRIVER_PLUGIN = '@pgpmjs/pglite-adapter';
|
|
6
6
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { checkOverwrite, writeControlFile, writeModule as writePackage } from '@pgpmjs/transform';
|
package/esm/utils/engine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
1
2
|
import { getEnvOptions } from '@pgpmjs/env';
|
|
2
3
|
import { BUILTIN_ENGINES, DEFAULT_ENGINE, } from '@pgpmjs/types';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
4
|
import { activateDriver, driverOverrideFromArgv, PGLITE_DRIVER_PLUGIN } from './driver';
|
|
5
5
|
/** Everything a real Postgres server can do — the built-in `pg` engine. */
|
|
6
6
|
export const SERVER_CAPABILITIES = {
|