@pgpmjs/core 7.2.1 → 7.3.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 +22 -0
- package/bundle/apply-envelope.d.ts +101 -0
- package/bundle/apply-envelope.js +136 -0
- package/bundle/index.d.ts +1 -0
- package/bundle/index.js +2 -0
- package/core/class/pgpm.d.ts +23 -1
- package/core/class/pgpm.js +36 -21
- package/esm/bundle/apply-envelope.js +130 -0
- package/esm/bundle/index.js +2 -0
- package/esm/core/class/pgpm.js +38 -23
- package/esm/projects/deploy.js +2 -2
- package/esm/projects/revert.js +1 -1
- package/esm/projects/verify.js +1 -1
- package/esm/resolution/deps.js +3 -2
- package/esm/utils/fs.js +16 -0
- package/esm/utils/glob.js +19 -0
- package/package.json +10 -10
- package/projects/deploy.js +2 -2
- package/projects/revert.js +1 -1
- package/projects/verify.js +1 -1
- package/resolution/deps.js +3 -2
- package/utils/fs.d.ts +5 -0
- package/utils/fs.js +23 -0
- package/utils/glob.d.ts +17 -0
- package/utils/glob.js +28 -0
package/README.md
CHANGED
|
@@ -22,6 +22,28 @@ pgpm Core is the main package for pgpm, providing tools for database migrations,
|
|
|
22
22
|
- Reading and writing SQL scripts
|
|
23
23
|
- Resolving dependencies between migrations
|
|
24
24
|
|
|
25
|
+
## Migration bundles and envelopes
|
|
26
|
+
|
|
27
|
+
`@pgpmjs/core` is the deployment half of the portable artifact layer defined in
|
|
28
|
+
`@pgpmjs/bundle` (the pure layer: `MigrationBundle` — a content-addressed snapshot of a
|
|
29
|
+
pgpm module — and `BundleEnvelope` — the versioned shipping container wrapping a schema
|
|
30
|
+
bundle plus data/fixtures SQL parts):
|
|
31
|
+
|
|
32
|
+
- `applyBundle(bundle, { config, dryRun?, verify?, timeoutMs?, validateReferences? })` —
|
|
33
|
+
safety-first apply of a `MigrationBundle` via `PgpmMigrate`: artifact-integrity and
|
|
34
|
+
optional namespace gating, dry-run preview, atomic single-transaction deploy,
|
|
35
|
+
timeout, and an explicit result including the rollback plan.
|
|
36
|
+
- `applyEnvelope(envelope, { config, dryRun?, verifyParts?, transformPartSql?, ... })` —
|
|
37
|
+
the generic `applyMigrationBundle` primitive: digest-gates the whole envelope
|
|
38
|
+
(`verifyEnvelope`), applies the schema via `applyBundle`, then replays each data and
|
|
39
|
+
fixtures part's deploy SQL in manifest order inside one transaction (a failing part
|
|
40
|
+
rolls back all part data; the schema stays applied). `previewEnvelopeApply` gives the
|
|
41
|
+
pure pre-flight: envelope issues, part replay order, and a layered rollback plan.
|
|
42
|
+
`transformPartSql` is the seam for pre-apply rewriting of part SQL (e.g.
|
|
43
|
+
deterministic ID remap).
|
|
44
|
+
|
|
45
|
+
See `pgpm/bundle/README.md` and the `pgpm-migration-bundle` skill for the artifact model.
|
|
46
|
+
|
|
25
47
|
## Configuration
|
|
26
48
|
|
|
27
49
|
### Error Output Configuration
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { BundleEnvelope, EnvelopeIssue, EnvelopePartKind } from '@pgpmjs/bundle';
|
|
2
|
+
import { ApplyBundleOptions, ApplyBundlePreview, ApplyBundleResult, PreviewBundleApplyOptions } from './apply';
|
|
3
|
+
/** One data/fixtures part in the envelope's replay order. */
|
|
4
|
+
export interface EnvelopePartRef {
|
|
5
|
+
kind: Exclude<EnvelopePartKind, 'schema'>;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
/** Pre-flight analysis of an envelope apply, computed without touching the database. */
|
|
9
|
+
export interface EnvelopeApplyPreview {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
envelopeDigest: string;
|
|
13
|
+
/** Digest/inventory/tamper problems from `verifyEnvelope`. */
|
|
14
|
+
envelopeIssues: EnvelopeIssue[];
|
|
15
|
+
/** The schema bundle's own pre-flight (integrity, namespace, order, rollback). */
|
|
16
|
+
schema: ApplyBundlePreview;
|
|
17
|
+
/** Data then fixtures parts, in manifest replay order. */
|
|
18
|
+
partOrder: EnvelopePartRef[];
|
|
19
|
+
/**
|
|
20
|
+
* How to undo this apply: part reverts in reverse replay order (parts
|
|
21
|
+
* without a revert script are flagged), then the schema rollback plan.
|
|
22
|
+
*/
|
|
23
|
+
rollbackPlan: {
|
|
24
|
+
parts: (EnvelopePartRef & {
|
|
25
|
+
hasRevert: boolean;
|
|
26
|
+
})[];
|
|
27
|
+
schema: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Analyze an envelope apply without executing it: verify the envelope's
|
|
32
|
+
* digest chain (including the schema bundle), run the schema pre-flight, and
|
|
33
|
+
* derive replay order + rollback plan. Pure — no database, no disk.
|
|
34
|
+
*/
|
|
35
|
+
export declare function previewEnvelopeApply(envelope: BundleEnvelope, options?: PreviewBundleApplyOptions): EnvelopeApplyPreview;
|
|
36
|
+
/** Context handed to {@link EnvelopeApplyOptions.transformPartSql}. */
|
|
37
|
+
export interface PartSqlContext {
|
|
38
|
+
kind: Exclude<EnvelopePartKind, 'schema'>;
|
|
39
|
+
name: string;
|
|
40
|
+
script: 'deploy' | 'verify';
|
|
41
|
+
}
|
|
42
|
+
/** Status/rollback metadata for one replayed part. */
|
|
43
|
+
export interface EnvelopePartResult {
|
|
44
|
+
kind: Exclude<EnvelopePartKind, 'schema'>;
|
|
45
|
+
name: string;
|
|
46
|
+
/** True when the part's deploy SQL ran against the database. */
|
|
47
|
+
executed: boolean;
|
|
48
|
+
/** Set only when `verifyParts` is enabled and the part has a verify script. */
|
|
49
|
+
verified?: boolean;
|
|
50
|
+
durationMs: number;
|
|
51
|
+
}
|
|
52
|
+
export interface EnvelopeApplyOptions extends ApplyBundleOptions {
|
|
53
|
+
/** Run each part's verify script after its deploy. Default false. */
|
|
54
|
+
verifyParts?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Seam for pre-apply rewriting of part SQL (e.g. deterministic ID remap).
|
|
57
|
+
* Applied to deploy and verify scripts just before execution; identity when
|
|
58
|
+
* omitted. Never sees schema-bundle SQL.
|
|
59
|
+
*/
|
|
60
|
+
transformPartSql?: (sql: string, ctx: PartSqlContext) => string;
|
|
61
|
+
}
|
|
62
|
+
/** Explicit outcome of an {@link applyEnvelope} call. */
|
|
63
|
+
export interface EnvelopeApplyResult {
|
|
64
|
+
preview: EnvelopeApplyPreview;
|
|
65
|
+
dryRun: boolean;
|
|
66
|
+
/** True when anything ran against the database. */
|
|
67
|
+
executed: boolean;
|
|
68
|
+
/** The schema half's own result (undefined on dry-run). */
|
|
69
|
+
schema?: ApplyBundleResult;
|
|
70
|
+
/** Per-part status metadata, in replay order (parts reached so far). */
|
|
71
|
+
parts: EnvelopePartResult[];
|
|
72
|
+
durationMs: number;
|
|
73
|
+
timedOut: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** Thrown when an envelope apply is refused pre-flight or fails mid-replay. */
|
|
76
|
+
export declare class EnvelopeApplyError extends Error {
|
|
77
|
+
readonly preview: EnvelopeApplyPreview;
|
|
78
|
+
/** Parts completed before the failure (for status recording). */
|
|
79
|
+
readonly parts: EnvelopePartResult[];
|
|
80
|
+
readonly timedOut: boolean;
|
|
81
|
+
constructor(message: string, preview: EnvelopeApplyPreview,
|
|
82
|
+
/** Parts completed before the failure (for status recording). */
|
|
83
|
+
parts?: EnvelopePartResult[], timedOut?: boolean);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Apply a {@link BundleEnvelope} into a target database: the generic
|
|
87
|
+
* `applyMigrationBundle` primitive.
|
|
88
|
+
*
|
|
89
|
+
* Order of operations:
|
|
90
|
+
* 1. Pre-flight: {@link previewEnvelopeApply} — refuse on any envelope digest
|
|
91
|
+
* issue unless `allowUnverified` (schema-bundle integrity and namespace
|
|
92
|
+
* gating are then enforced again by {@link applyBundle}).
|
|
93
|
+
* 2. Schema: {@link applyBundle} on `envelope.schema` (atomic transaction,
|
|
94
|
+
* timeout, dry-run all handled there).
|
|
95
|
+
* 3. Parts: replay each data then fixtures part's deploy SQL in manifest
|
|
96
|
+
* order, inside a single transaction so a failing part rolls back all part
|
|
97
|
+
* data (the schema stays applied; the preview's rollback plan describes the
|
|
98
|
+
* full undo). Optional per-part verify. The `transformPartSql` seam runs
|
|
99
|
+
* just before execution.
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyEnvelope(envelope: BundleEnvelope, options: EnvelopeApplyOptions): Promise<EnvelopeApplyResult>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnvelopeApplyError = void 0;
|
|
4
|
+
exports.previewEnvelopeApply = previewEnvelopeApply;
|
|
5
|
+
exports.applyEnvelope = applyEnvelope;
|
|
6
|
+
const pg_cache_1 = require("pg-cache");
|
|
7
|
+
const bundle_1 = require("@pgpmjs/bundle");
|
|
8
|
+
const apply_1 = require("./apply");
|
|
9
|
+
/**
|
|
10
|
+
* Analyze an envelope apply without executing it: verify the envelope's
|
|
11
|
+
* digest chain (including the schema bundle), run the schema pre-flight, and
|
|
12
|
+
* derive replay order + rollback plan. Pure — no database, no disk.
|
|
13
|
+
*/
|
|
14
|
+
function previewEnvelopeApply(envelope, options = {}) {
|
|
15
|
+
const { issues } = (0, bundle_1.verifyEnvelope)(envelope);
|
|
16
|
+
const parts = [
|
|
17
|
+
...envelope.data.map(part => ({ kind: 'data', name: part.name, part })),
|
|
18
|
+
...envelope.fixtures.map(part => ({ kind: 'fixtures', name: part.name, part }))
|
|
19
|
+
];
|
|
20
|
+
const schema = (0, apply_1.previewBundleApply)(envelope.schema, options);
|
|
21
|
+
return {
|
|
22
|
+
name: envelope.manifest.name,
|
|
23
|
+
version: envelope.manifest.version,
|
|
24
|
+
envelopeDigest: envelope.manifest.digest,
|
|
25
|
+
envelopeIssues: issues,
|
|
26
|
+
schema,
|
|
27
|
+
partOrder: parts.map(({ kind, name }) => ({ kind, name })),
|
|
28
|
+
rollbackPlan: {
|
|
29
|
+
parts: [...parts]
|
|
30
|
+
.reverse()
|
|
31
|
+
.map(({ kind, name, part }) => ({ kind, name, hasRevert: part.revert !== null })),
|
|
32
|
+
schema: schema.rollbackPlan
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Thrown when an envelope apply is refused pre-flight or fails mid-replay. */
|
|
37
|
+
class EnvelopeApplyError extends Error {
|
|
38
|
+
preview;
|
|
39
|
+
parts;
|
|
40
|
+
timedOut;
|
|
41
|
+
constructor(message, preview,
|
|
42
|
+
/** Parts completed before the failure (for status recording). */
|
|
43
|
+
parts = [], timedOut = false) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.preview = preview;
|
|
46
|
+
this.parts = parts;
|
|
47
|
+
this.timedOut = timedOut;
|
|
48
|
+
this.name = 'EnvelopeApplyError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.EnvelopeApplyError = EnvelopeApplyError;
|
|
52
|
+
/**
|
|
53
|
+
* Apply a {@link BundleEnvelope} into a target database: the generic
|
|
54
|
+
* `applyMigrationBundle` primitive.
|
|
55
|
+
*
|
|
56
|
+
* Order of operations:
|
|
57
|
+
* 1. Pre-flight: {@link previewEnvelopeApply} — refuse on any envelope digest
|
|
58
|
+
* issue unless `allowUnverified` (schema-bundle integrity and namespace
|
|
59
|
+
* gating are then enforced again by {@link applyBundle}).
|
|
60
|
+
* 2. Schema: {@link applyBundle} on `envelope.schema` (atomic transaction,
|
|
61
|
+
* timeout, dry-run all handled there).
|
|
62
|
+
* 3. Parts: replay each data then fixtures part's deploy SQL in manifest
|
|
63
|
+
* order, inside a single transaction so a failing part rolls back all part
|
|
64
|
+
* data (the schema stays applied; the preview's rollback plan describes the
|
|
65
|
+
* full undo). Optional per-part verify. The `transformPartSql` seam runs
|
|
66
|
+
* just before execution.
|
|
67
|
+
*/
|
|
68
|
+
async function applyEnvelope(envelope, options) {
|
|
69
|
+
const started = Date.now();
|
|
70
|
+
const preview = previewEnvelopeApply(envelope, options);
|
|
71
|
+
if (!options.allowUnverified && preview.envelopeIssues.length > 0) {
|
|
72
|
+
throw new EnvelopeApplyError(`Refusing to apply envelope "${preview.name}@${preview.version}": failed integrity ` +
|
|
73
|
+
`verification (${preview.envelopeIssues.length} issue(s): ` +
|
|
74
|
+
`${preview.envelopeIssues.map(i => i.kind).join(', ')})`, preview);
|
|
75
|
+
}
|
|
76
|
+
if (options.dryRun) {
|
|
77
|
+
return {
|
|
78
|
+
preview,
|
|
79
|
+
dryRun: true,
|
|
80
|
+
executed: false,
|
|
81
|
+
parts: [],
|
|
82
|
+
durationMs: Date.now() - started,
|
|
83
|
+
timedOut: false
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const schema = await (0, apply_1.applyBundle)(envelope.schema, options);
|
|
87
|
+
const transform = options.transformPartSql ?? ((sql) => sql);
|
|
88
|
+
const orderedParts = [
|
|
89
|
+
...envelope.data.map(part => ({ kind: 'data', name: part.name, part })),
|
|
90
|
+
...envelope.fixtures.map(part => ({ kind: 'fixtures', name: part.name, part }))
|
|
91
|
+
];
|
|
92
|
+
const parts = [];
|
|
93
|
+
if (orderedParts.length > 0) {
|
|
94
|
+
const pool = (0, pg_cache_1.getPgPool)(options.config);
|
|
95
|
+
const client = await pool.connect();
|
|
96
|
+
try {
|
|
97
|
+
await client.query('BEGIN');
|
|
98
|
+
for (const { kind, name, part } of orderedParts) {
|
|
99
|
+
const partStarted = Date.now();
|
|
100
|
+
try {
|
|
101
|
+
await client.query(transform(part.deploy, { kind, name, script: 'deploy' }));
|
|
102
|
+
const result = {
|
|
103
|
+
kind,
|
|
104
|
+
name,
|
|
105
|
+
executed: true,
|
|
106
|
+
durationMs: Date.now() - partStarted
|
|
107
|
+
};
|
|
108
|
+
if (options.verifyParts && part.verify !== null) {
|
|
109
|
+
await client.query(transform(part.verify, { kind, name, script: 'verify' }));
|
|
110
|
+
result.verified = true;
|
|
111
|
+
}
|
|
112
|
+
parts.push(result);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
await client.query('ROLLBACK');
|
|
116
|
+
throw new EnvelopeApplyError(`Part ${kind}/${name} of envelope "${preview.name}@${preview.version}" failed: ` +
|
|
117
|
+
`${e instanceof Error ? e.message : String(e)} — all part data rolled back ` +
|
|
118
|
+
`(schema remains applied; see preview.rollbackPlan)`, preview, parts);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await client.query('COMMIT');
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
client.release();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
preview,
|
|
129
|
+
dryRun: false,
|
|
130
|
+
executed: true,
|
|
131
|
+
schema,
|
|
132
|
+
parts,
|
|
133
|
+
durationMs: Date.now() - started,
|
|
134
|
+
timedOut: schema.timedOut
|
|
135
|
+
};
|
|
136
|
+
}
|
package/bundle/index.d.ts
CHANGED
package/bundle/index.js
CHANGED
|
@@ -29,3 +29,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
__exportStar(require("@pgpmjs/bundle"), exports);
|
|
30
30
|
// applyBundle stays in core: it drives PgpmMigrate (the deploy engine).
|
|
31
31
|
__exportStar(require("./apply"), exports);
|
|
32
|
+
// applyEnvelope: schema apply + data/fixtures part replay for a BundleEnvelope.
|
|
33
|
+
__exportStar(require("./apply-envelope"), exports);
|
package/core/class/pgpm.d.ts
CHANGED
|
@@ -3,6 +3,17 @@ import { PgConfig } from 'pg-env';
|
|
|
3
3
|
import { ExtensionInfo } from '@pgpmjs/ast/files';
|
|
4
4
|
import { PackageAnalysisResult, RenameOptions } from '@pgpmjs/ast/files/types';
|
|
5
5
|
import { ModuleMap } from '../../modules/modules';
|
|
6
|
+
/**
|
|
7
|
+
* Options accepted by the {@link PgpmPackage} constructor.
|
|
8
|
+
*/
|
|
9
|
+
export interface PgpmPackageOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Directory (relative to the workspace root) where pgpm modules are installed.
|
|
12
|
+
* Takes precedence over `PGPM_EXTENSIONS_DIR` and the `extensionsDir` config
|
|
13
|
+
* field. Defaults to `extensions`.
|
|
14
|
+
*/
|
|
15
|
+
extensionsDir?: string;
|
|
16
|
+
}
|
|
6
17
|
export declare enum PackageContext {
|
|
7
18
|
Outside = "outside",
|
|
8
19
|
Workspace = "workspace-root",
|
|
@@ -49,10 +60,21 @@ export declare class PgpmPackage {
|
|
|
49
60
|
config?: PgpmWorkspaceConfig;
|
|
50
61
|
allowedDirs: string[];
|
|
51
62
|
allowedParentDirs: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Directory name (relative to the workspace root) where pgpm modules are installed.
|
|
65
|
+
* Resolved from the constructor option, `PGPM_EXTENSIONS_DIR`, the `extensionsDir`
|
|
66
|
+
* config field, then the `extensions` default.
|
|
67
|
+
*/
|
|
68
|
+
extensionsDir: string;
|
|
69
|
+
private readonly options;
|
|
52
70
|
private _moduleMap?;
|
|
53
71
|
private _moduleInfo?;
|
|
54
|
-
constructor(cwd?: string);
|
|
72
|
+
constructor(cwd?: string, options?: PgpmPackageOptions);
|
|
55
73
|
resetCwd(cwd: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Absolute path of the workspace directory where pgpm modules are installed.
|
|
76
|
+
*/
|
|
77
|
+
getExtensionsPath(): string;
|
|
56
78
|
private resolveSqitchPath;
|
|
57
79
|
private loadConfigSync;
|
|
58
80
|
private loadAllowedDirs;
|
package/core/class/pgpm.js
CHANGED
|
@@ -59,14 +59,11 @@ const client_1 = require("../../migrate/client");
|
|
|
59
59
|
const modules_1 = require("../../modules/modules");
|
|
60
60
|
const package_1 = require("../../packaging/package");
|
|
61
61
|
const deps_1 = require("../../resolution/deps");
|
|
62
|
+
const fs_2 = require("../../utils/fs");
|
|
63
|
+
const glob_1 = require("../../utils/glob");
|
|
62
64
|
const target_utils_1 = require("../../utils/target-utils");
|
|
63
65
|
const template_scaffold_1 = require("../template-scaffold");
|
|
64
66
|
const logger = new logger_1.Logger('pgpm');
|
|
65
|
-
/**
|
|
66
|
-
* Directory name for workspace extensions.
|
|
67
|
-
* Extensions are installed globally in the workspace's extensions/ directory.
|
|
68
|
-
*/
|
|
69
|
-
const EXTENSIONS_DIR = 'extensions';
|
|
70
67
|
function sortObjectByKey(obj) {
|
|
71
68
|
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
72
69
|
}
|
|
@@ -102,21 +99,37 @@ class PgpmPackage {
|
|
|
102
99
|
config;
|
|
103
100
|
allowedDirs = [];
|
|
104
101
|
allowedParentDirs = [];
|
|
102
|
+
/**
|
|
103
|
+
* Directory name (relative to the workspace root) where pgpm modules are installed.
|
|
104
|
+
* Resolved from the constructor option, `PGPM_EXTENSIONS_DIR`, the `extensionsDir`
|
|
105
|
+
* config field, then the `extensions` default.
|
|
106
|
+
*/
|
|
107
|
+
extensionsDir = types_1.DEFAULT_EXTENSIONS_DIR;
|
|
108
|
+
options;
|
|
105
109
|
_moduleMap;
|
|
106
110
|
_moduleInfo;
|
|
107
|
-
constructor(cwd = process.cwd()) {
|
|
111
|
+
constructor(cwd = process.cwd(), options = {}) {
|
|
112
|
+
this.options = options;
|
|
108
113
|
this.resetCwd(cwd);
|
|
109
114
|
}
|
|
110
115
|
resetCwd(cwd) {
|
|
111
116
|
this.cwd = cwd;
|
|
112
117
|
this.workspacePath = (0, env_1.resolvePgpmPath)(this.cwd);
|
|
113
118
|
this.modulePath = this.resolveSqitchPath();
|
|
119
|
+
this.extensionsDir = (0, env_1.getExtensionsDir)(this.options.extensionsDir, this.workspacePath ?? this.cwd);
|
|
114
120
|
if (this.workspacePath) {
|
|
115
121
|
this.config = this.loadConfigSync();
|
|
116
122
|
this.allowedDirs = this.loadAllowedDirs();
|
|
117
123
|
this.allowedParentDirs = this.loadAllowedParentDirs();
|
|
118
124
|
}
|
|
119
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Absolute path of the workspace directory where pgpm modules are installed.
|
|
128
|
+
*/
|
|
129
|
+
getExtensionsPath() {
|
|
130
|
+
this.ensureWorkspace();
|
|
131
|
+
return path_1.default.join(this.workspacePath, this.extensionsDir);
|
|
132
|
+
}
|
|
120
133
|
resolveSqitchPath() {
|
|
121
134
|
try {
|
|
122
135
|
return (0, env_1.walkUp)(this.cwd, 'pgpm.plan');
|
|
@@ -130,7 +143,7 @@ class PgpmPackage {
|
|
|
130
143
|
}
|
|
131
144
|
loadAllowedDirs() {
|
|
132
145
|
const globs = this.config?.packages ?? [];
|
|
133
|
-
const dirs = globs.flatMap(pattern =>
|
|
146
|
+
const dirs = globs.flatMap(pattern => (0, glob_1.globPaths)(this.workspacePath, pattern));
|
|
134
147
|
const resolvedDirs = dirs.map(dir => path_1.default.resolve(dir));
|
|
135
148
|
// Remove duplicates by converting to Set and back to array
|
|
136
149
|
return [...new Set(resolvedDirs.sort((a, b) => a.localeCompare(b)))];
|
|
@@ -222,7 +235,7 @@ class PgpmPackage {
|
|
|
222
235
|
const dirs = this.loadAllowedDirs();
|
|
223
236
|
const results = [];
|
|
224
237
|
for (const dir of dirs) {
|
|
225
|
-
const proj = new PgpmPackage(dir);
|
|
238
|
+
const proj = new PgpmPackage(dir, this.options);
|
|
226
239
|
if (proj.isInModule()) {
|
|
227
240
|
results.push(proj);
|
|
228
241
|
}
|
|
@@ -242,9 +255,9 @@ class PgpmPackage {
|
|
|
242
255
|
// nested workspaces (e.g. test fixtures) out of the module map.
|
|
243
256
|
const packageDirs = [
|
|
244
257
|
...this.allowedDirs,
|
|
245
|
-
...
|
|
258
|
+
...(0, glob_1.globPaths)(this.workspacePath, this.extensionsDir, '{*,@*/*}')
|
|
246
259
|
];
|
|
247
|
-
const moduleFiles = [...new Set(packageDirs.flatMap(dir => glob.sync(
|
|
260
|
+
const moduleFiles = [...new Set(packageDirs.flatMap(dir => glob.sync((0, glob_1.globPattern)(dir, '**/*.control'))))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
248
261
|
// Group files by module name to handle collisions
|
|
249
262
|
const filesByName = new Map();
|
|
250
263
|
moduleFiles.forEach((file) => {
|
|
@@ -295,7 +308,7 @@ class PgpmPackage {
|
|
|
295
308
|
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
296
309
|
}
|
|
297
310
|
const modulePath = path_1.default.resolve(this.workspacePath, modules[name].path);
|
|
298
|
-
return new PgpmPackage(modulePath);
|
|
311
|
+
return new PgpmPackage(modulePath, this.options);
|
|
299
312
|
}
|
|
300
313
|
// ──────────────── Module-scoped ────────────────
|
|
301
314
|
getModuleInfo() {
|
|
@@ -810,7 +823,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
810
823
|
this.ensureWorkspace();
|
|
811
824
|
const inModule = this.isInModule();
|
|
812
825
|
const originalDir = process.cwd();
|
|
813
|
-
const skitchExtDir =
|
|
826
|
+
const skitchExtDir = this.getExtensionsPath();
|
|
814
827
|
let pkgJsonPath;
|
|
815
828
|
let pkgData;
|
|
816
829
|
if (inModule) {
|
|
@@ -828,27 +841,27 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
828
841
|
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'pgpm-install-'));
|
|
829
842
|
try {
|
|
830
843
|
process.chdir(tempDir);
|
|
831
|
-
(0, child_process_1.execSync)(`npm install ${pkgstr} --production --prefix
|
|
844
|
+
(0, child_process_1.execSync)(`npm install ${pkgstr} --production --prefix ./${this.extensionsDir}`, {
|
|
832
845
|
stdio: 'inherit'
|
|
833
846
|
});
|
|
834
|
-
const matches = glob.sync('
|
|
847
|
+
const matches = glob.sync((0, glob_1.globPattern)('.', this.extensionsDir, '**/pgpm.plan'));
|
|
835
848
|
const installs = matches.map((conf) => {
|
|
836
849
|
const fullConf = (0, path_1.resolve)(conf);
|
|
837
850
|
const extDir = (0, path_1.dirname)(fullConf);
|
|
838
|
-
const parts = extDir.split('node_modules/');
|
|
851
|
+
const parts = (0, glob_1.toPosixPath)(extDir).split('node_modules/');
|
|
839
852
|
const relativeDir = parts[parts.length - 1];
|
|
840
853
|
const dstDir = path_1.default.join(skitchExtDir, relativeDir);
|
|
841
854
|
return { src: extDir, dst: dstDir, pkg: relativeDir };
|
|
842
855
|
});
|
|
843
856
|
// Sort deepest paths first so nested node_modules deps are
|
|
844
857
|
// extracted before their parent directories are moved.
|
|
845
|
-
installs.sort((a, b) => b.src.split('/').length - a.src.split('/').length);
|
|
858
|
+
installs.sort((a, b) => (0, glob_1.toPosixPath)(b.src).split('/').length - (0, glob_1.toPosixPath)(a.src).split('/').length);
|
|
846
859
|
for (const { src, dst, pkg } of installs) {
|
|
847
860
|
if (fs_1.default.existsSync(dst)) {
|
|
848
861
|
fs_1.default.rmSync(dst, { recursive: true, force: true });
|
|
849
862
|
}
|
|
850
863
|
fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
|
|
851
|
-
(0,
|
|
864
|
+
(0, fs_2.movePath)(src, dst);
|
|
852
865
|
logger.success(`✔ installed ${pkg}`);
|
|
853
866
|
const pkgJsonFile = path_1.default.join(dst, 'package.json');
|
|
854
867
|
if (!fs_1.default.existsSync(pkgJsonFile)) {
|
|
@@ -864,8 +877,9 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
864
877
|
}
|
|
865
878
|
}
|
|
866
879
|
finally {
|
|
867
|
-
|
|
880
|
+
// chdir out first: Windows locks the process working directory
|
|
868
881
|
process.chdir(originalDir);
|
|
882
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
869
883
|
}
|
|
870
884
|
}
|
|
871
885
|
if (inModule && pkgData && pkgJsonPath) {
|
|
@@ -985,7 +999,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
985
999
|
}
|
|
986
1000
|
const pkgData = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
987
1001
|
const dependencies = pkgData.dependencies || {};
|
|
988
|
-
const skitchExtDir =
|
|
1002
|
+
const skitchExtDir = this.getExtensionsPath();
|
|
989
1003
|
const installed = [];
|
|
990
1004
|
const installedVersions = {};
|
|
991
1005
|
for (const [name, version] of Object.entries(dependencies)) {
|
|
@@ -1008,7 +1022,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1008
1022
|
*/
|
|
1009
1023
|
getWorkspaceInstalledModules() {
|
|
1010
1024
|
this.ensureWorkspace();
|
|
1011
|
-
const extensionsDir =
|
|
1025
|
+
const extensionsDir = this.getExtensionsPath();
|
|
1012
1026
|
if (!fs_1.default.existsSync(extensionsDir)) {
|
|
1013
1027
|
return [];
|
|
1014
1028
|
}
|
|
@@ -1659,7 +1673,8 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1659
1673
|
if (base !== expected)
|
|
1660
1674
|
issues.push({ code: 'control_filename_mismatch', message: `Control filename ${base} != ${expected}`, file: controlPath });
|
|
1661
1675
|
}
|
|
1662
|
-
|
|
1676
|
+
const reported = issues.map(issue => ({ ...issue, file: (0, glob_1.toPosixPath)(issue.file) }));
|
|
1677
|
+
return { ok: reported.length === 0, name: info.extname, path: (0, glob_1.toPosixPath)(modPath), issues: reported };
|
|
1663
1678
|
}
|
|
1664
1679
|
renameModule(newName, opts) {
|
|
1665
1680
|
this.ensureModule();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getPgPool } from 'pg-cache';
|
|
2
|
+
import { verifyEnvelope } from '@pgpmjs/bundle';
|
|
3
|
+
import { applyBundle, previewBundleApply } from './apply';
|
|
4
|
+
/**
|
|
5
|
+
* Analyze an envelope apply without executing it: verify the envelope's
|
|
6
|
+
* digest chain (including the schema bundle), run the schema pre-flight, and
|
|
7
|
+
* derive replay order + rollback plan. Pure — no database, no disk.
|
|
8
|
+
*/
|
|
9
|
+
export function previewEnvelopeApply(envelope, options = {}) {
|
|
10
|
+
const { issues } = verifyEnvelope(envelope);
|
|
11
|
+
const parts = [
|
|
12
|
+
...envelope.data.map(part => ({ kind: 'data', name: part.name, part })),
|
|
13
|
+
...envelope.fixtures.map(part => ({ kind: 'fixtures', name: part.name, part }))
|
|
14
|
+
];
|
|
15
|
+
const schema = previewBundleApply(envelope.schema, options);
|
|
16
|
+
return {
|
|
17
|
+
name: envelope.manifest.name,
|
|
18
|
+
version: envelope.manifest.version,
|
|
19
|
+
envelopeDigest: envelope.manifest.digest,
|
|
20
|
+
envelopeIssues: issues,
|
|
21
|
+
schema,
|
|
22
|
+
partOrder: parts.map(({ kind, name }) => ({ kind, name })),
|
|
23
|
+
rollbackPlan: {
|
|
24
|
+
parts: [...parts]
|
|
25
|
+
.reverse()
|
|
26
|
+
.map(({ kind, name, part }) => ({ kind, name, hasRevert: part.revert !== null })),
|
|
27
|
+
schema: schema.rollbackPlan
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Thrown when an envelope apply is refused pre-flight or fails mid-replay. */
|
|
32
|
+
export class EnvelopeApplyError extends Error {
|
|
33
|
+
preview;
|
|
34
|
+
parts;
|
|
35
|
+
timedOut;
|
|
36
|
+
constructor(message, preview,
|
|
37
|
+
/** Parts completed before the failure (for status recording). */
|
|
38
|
+
parts = [], timedOut = false) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.preview = preview;
|
|
41
|
+
this.parts = parts;
|
|
42
|
+
this.timedOut = timedOut;
|
|
43
|
+
this.name = 'EnvelopeApplyError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Apply a {@link BundleEnvelope} into a target database: the generic
|
|
48
|
+
* `applyMigrationBundle` primitive.
|
|
49
|
+
*
|
|
50
|
+
* Order of operations:
|
|
51
|
+
* 1. Pre-flight: {@link previewEnvelopeApply} — refuse on any envelope digest
|
|
52
|
+
* issue unless `allowUnverified` (schema-bundle integrity and namespace
|
|
53
|
+
* gating are then enforced again by {@link applyBundle}).
|
|
54
|
+
* 2. Schema: {@link applyBundle} on `envelope.schema` (atomic transaction,
|
|
55
|
+
* timeout, dry-run all handled there).
|
|
56
|
+
* 3. Parts: replay each data then fixtures part's deploy SQL in manifest
|
|
57
|
+
* order, inside a single transaction so a failing part rolls back all part
|
|
58
|
+
* data (the schema stays applied; the preview's rollback plan describes the
|
|
59
|
+
* full undo). Optional per-part verify. The `transformPartSql` seam runs
|
|
60
|
+
* just before execution.
|
|
61
|
+
*/
|
|
62
|
+
export async function applyEnvelope(envelope, options) {
|
|
63
|
+
const started = Date.now();
|
|
64
|
+
const preview = previewEnvelopeApply(envelope, options);
|
|
65
|
+
if (!options.allowUnverified && preview.envelopeIssues.length > 0) {
|
|
66
|
+
throw new EnvelopeApplyError(`Refusing to apply envelope "${preview.name}@${preview.version}": failed integrity ` +
|
|
67
|
+
`verification (${preview.envelopeIssues.length} issue(s): ` +
|
|
68
|
+
`${preview.envelopeIssues.map(i => i.kind).join(', ')})`, preview);
|
|
69
|
+
}
|
|
70
|
+
if (options.dryRun) {
|
|
71
|
+
return {
|
|
72
|
+
preview,
|
|
73
|
+
dryRun: true,
|
|
74
|
+
executed: false,
|
|
75
|
+
parts: [],
|
|
76
|
+
durationMs: Date.now() - started,
|
|
77
|
+
timedOut: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const schema = await applyBundle(envelope.schema, options);
|
|
81
|
+
const transform = options.transformPartSql ?? ((sql) => sql);
|
|
82
|
+
const orderedParts = [
|
|
83
|
+
...envelope.data.map(part => ({ kind: 'data', name: part.name, part })),
|
|
84
|
+
...envelope.fixtures.map(part => ({ kind: 'fixtures', name: part.name, part }))
|
|
85
|
+
];
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (orderedParts.length > 0) {
|
|
88
|
+
const pool = getPgPool(options.config);
|
|
89
|
+
const client = await pool.connect();
|
|
90
|
+
try {
|
|
91
|
+
await client.query('BEGIN');
|
|
92
|
+
for (const { kind, name, part } of orderedParts) {
|
|
93
|
+
const partStarted = Date.now();
|
|
94
|
+
try {
|
|
95
|
+
await client.query(transform(part.deploy, { kind, name, script: 'deploy' }));
|
|
96
|
+
const result = {
|
|
97
|
+
kind,
|
|
98
|
+
name,
|
|
99
|
+
executed: true,
|
|
100
|
+
durationMs: Date.now() - partStarted
|
|
101
|
+
};
|
|
102
|
+
if (options.verifyParts && part.verify !== null) {
|
|
103
|
+
await client.query(transform(part.verify, { kind, name, script: 'verify' }));
|
|
104
|
+
result.verified = true;
|
|
105
|
+
}
|
|
106
|
+
parts.push(result);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
await client.query('ROLLBACK');
|
|
110
|
+
throw new EnvelopeApplyError(`Part ${kind}/${name} of envelope "${preview.name}@${preview.version}" failed: ` +
|
|
111
|
+
`${e instanceof Error ? e.message : String(e)} — all part data rolled back ` +
|
|
112
|
+
`(schema remains applied; see preview.rollbackPlan)`, preview, parts);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
await client.query('COMMIT');
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
client.release();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
preview,
|
|
123
|
+
dryRun: false,
|
|
124
|
+
executed: true,
|
|
125
|
+
schema,
|
|
126
|
+
parts,
|
|
127
|
+
durationMs: Date.now() - started,
|
|
128
|
+
timedOut: schema.timedOut
|
|
129
|
+
};
|
|
130
|
+
}
|
package/esm/bundle/index.js
CHANGED
package/esm/core/class/pgpm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { loadConfigSyncFromDir, resolvePgpmPath, walkUp } from '@pgpmjs/env';
|
|
1
|
+
import { getExtensionsDir, loadConfigSyncFromDir, resolvePgpmPath, walkUp } from '@pgpmjs/env';
|
|
2
2
|
import { Logger } from '@pgpmjs/logger';
|
|
3
|
-
import { errors } from '@pgpmjs/types';
|
|
3
|
+
import { DEFAULT_EXTENSIONS_DIR, errors } from '@pgpmjs/types';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import * as glob from 'glob';
|
|
@@ -20,14 +20,11 @@ import { PgpmMigrate } from '../../migrate/client';
|
|
|
20
20
|
import { getExtensionsAndModules, getExtensionsAndModulesChanges, latestChange, latestChangeAndVersion } from '../../modules/modules';
|
|
21
21
|
import { packageModule } from '../../packaging/package';
|
|
22
22
|
import { resolveDependencies, resolveExtensionDependencies } from '../../resolution/deps';
|
|
23
|
+
import { movePath } from '../../utils/fs';
|
|
24
|
+
import { globPaths, globPattern, toPosixPath } from '../../utils/glob';
|
|
23
25
|
import { parseTarget } from '../../utils/target-utils';
|
|
24
26
|
import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, DEFAULT_TEMPLATE_TTL_MS, scaffoldTemplate } from '../template-scaffold';
|
|
25
27
|
const logger = new Logger('pgpm');
|
|
26
|
-
/**
|
|
27
|
-
* Directory name for workspace extensions.
|
|
28
|
-
* Extensions are installed globally in the workspace's extensions/ directory.
|
|
29
|
-
*/
|
|
30
|
-
const EXTENSIONS_DIR = 'extensions';
|
|
31
28
|
function sortObjectByKey(obj) {
|
|
32
29
|
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
33
30
|
}
|
|
@@ -63,21 +60,37 @@ export class PgpmPackage {
|
|
|
63
60
|
config;
|
|
64
61
|
allowedDirs = [];
|
|
65
62
|
allowedParentDirs = [];
|
|
63
|
+
/**
|
|
64
|
+
* Directory name (relative to the workspace root) where pgpm modules are installed.
|
|
65
|
+
* Resolved from the constructor option, `PGPM_EXTENSIONS_DIR`, the `extensionsDir`
|
|
66
|
+
* config field, then the `extensions` default.
|
|
67
|
+
*/
|
|
68
|
+
extensionsDir = DEFAULT_EXTENSIONS_DIR;
|
|
69
|
+
options;
|
|
66
70
|
_moduleMap;
|
|
67
71
|
_moduleInfo;
|
|
68
|
-
constructor(cwd = process.cwd()) {
|
|
72
|
+
constructor(cwd = process.cwd(), options = {}) {
|
|
73
|
+
this.options = options;
|
|
69
74
|
this.resetCwd(cwd);
|
|
70
75
|
}
|
|
71
76
|
resetCwd(cwd) {
|
|
72
77
|
this.cwd = cwd;
|
|
73
78
|
this.workspacePath = resolvePgpmPath(this.cwd);
|
|
74
79
|
this.modulePath = this.resolveSqitchPath();
|
|
80
|
+
this.extensionsDir = getExtensionsDir(this.options.extensionsDir, this.workspacePath ?? this.cwd);
|
|
75
81
|
if (this.workspacePath) {
|
|
76
82
|
this.config = this.loadConfigSync();
|
|
77
83
|
this.allowedDirs = this.loadAllowedDirs();
|
|
78
84
|
this.allowedParentDirs = this.loadAllowedParentDirs();
|
|
79
85
|
}
|
|
80
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Absolute path of the workspace directory where pgpm modules are installed.
|
|
89
|
+
*/
|
|
90
|
+
getExtensionsPath() {
|
|
91
|
+
this.ensureWorkspace();
|
|
92
|
+
return path.join(this.workspacePath, this.extensionsDir);
|
|
93
|
+
}
|
|
81
94
|
resolveSqitchPath() {
|
|
82
95
|
try {
|
|
83
96
|
return walkUp(this.cwd, 'pgpm.plan');
|
|
@@ -91,7 +104,7 @@ export class PgpmPackage {
|
|
|
91
104
|
}
|
|
92
105
|
loadAllowedDirs() {
|
|
93
106
|
const globs = this.config?.packages ?? [];
|
|
94
|
-
const dirs = globs.flatMap(pattern =>
|
|
107
|
+
const dirs = globs.flatMap(pattern => globPaths(this.workspacePath, pattern));
|
|
95
108
|
const resolvedDirs = dirs.map(dir => path.resolve(dir));
|
|
96
109
|
// Remove duplicates by converting to Set and back to array
|
|
97
110
|
return [...new Set(resolvedDirs.sort((a, b) => a.localeCompare(b)))];
|
|
@@ -183,7 +196,7 @@ export class PgpmPackage {
|
|
|
183
196
|
const dirs = this.loadAllowedDirs();
|
|
184
197
|
const results = [];
|
|
185
198
|
for (const dir of dirs) {
|
|
186
|
-
const proj = new PgpmPackage(dir);
|
|
199
|
+
const proj = new PgpmPackage(dir, this.options);
|
|
187
200
|
if (proj.isInModule()) {
|
|
188
201
|
results.push(proj);
|
|
189
202
|
}
|
|
@@ -203,9 +216,9 @@ export class PgpmPackage {
|
|
|
203
216
|
// nested workspaces (e.g. test fixtures) out of the module map.
|
|
204
217
|
const packageDirs = [
|
|
205
218
|
...this.allowedDirs,
|
|
206
|
-
...
|
|
219
|
+
...globPaths(this.workspacePath, this.extensionsDir, '{*,@*/*}')
|
|
207
220
|
];
|
|
208
|
-
const moduleFiles = [...new Set(packageDirs.flatMap(dir => glob.sync(
|
|
221
|
+
const moduleFiles = [...new Set(packageDirs.flatMap(dir => glob.sync(globPattern(dir, '**/*.control'))))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
209
222
|
// Group files by module name to handle collisions
|
|
210
223
|
const filesByName = new Map();
|
|
211
224
|
moduleFiles.forEach((file) => {
|
|
@@ -256,7 +269,7 @@ export class PgpmPackage {
|
|
|
256
269
|
throw errors.MODULE_NOT_FOUND({ name });
|
|
257
270
|
}
|
|
258
271
|
const modulePath = path.resolve(this.workspacePath, modules[name].path);
|
|
259
|
-
return new PgpmPackage(modulePath);
|
|
272
|
+
return new PgpmPackage(modulePath, this.options);
|
|
260
273
|
}
|
|
261
274
|
// ──────────────── Module-scoped ────────────────
|
|
262
275
|
getModuleInfo() {
|
|
@@ -771,7 +784,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
771
784
|
this.ensureWorkspace();
|
|
772
785
|
const inModule = this.isInModule();
|
|
773
786
|
const originalDir = process.cwd();
|
|
774
|
-
const skitchExtDir =
|
|
787
|
+
const skitchExtDir = this.getExtensionsPath();
|
|
775
788
|
let pkgJsonPath;
|
|
776
789
|
let pkgData;
|
|
777
790
|
if (inModule) {
|
|
@@ -789,27 +802,27 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
789
802
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-install-'));
|
|
790
803
|
try {
|
|
791
804
|
process.chdir(tempDir);
|
|
792
|
-
execSync(`npm install ${pkgstr} --production --prefix
|
|
805
|
+
execSync(`npm install ${pkgstr} --production --prefix ./${this.extensionsDir}`, {
|
|
793
806
|
stdio: 'inherit'
|
|
794
807
|
});
|
|
795
|
-
const matches = glob.sync('
|
|
808
|
+
const matches = glob.sync(globPattern('.', this.extensionsDir, '**/pgpm.plan'));
|
|
796
809
|
const installs = matches.map((conf) => {
|
|
797
810
|
const fullConf = resolve(conf);
|
|
798
811
|
const extDir = dirname(fullConf);
|
|
799
|
-
const parts = extDir.split('node_modules/');
|
|
812
|
+
const parts = toPosixPath(extDir).split('node_modules/');
|
|
800
813
|
const relativeDir = parts[parts.length - 1];
|
|
801
814
|
const dstDir = path.join(skitchExtDir, relativeDir);
|
|
802
815
|
return { src: extDir, dst: dstDir, pkg: relativeDir };
|
|
803
816
|
});
|
|
804
817
|
// Sort deepest paths first so nested node_modules deps are
|
|
805
818
|
// extracted before their parent directories are moved.
|
|
806
|
-
installs.sort((a, b) => b.src.split('/').length - a.src.split('/').length);
|
|
819
|
+
installs.sort((a, b) => toPosixPath(b.src).split('/').length - toPosixPath(a.src).split('/').length);
|
|
807
820
|
for (const { src, dst, pkg } of installs) {
|
|
808
821
|
if (fs.existsSync(dst)) {
|
|
809
822
|
fs.rmSync(dst, { recursive: true, force: true });
|
|
810
823
|
}
|
|
811
824
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
812
|
-
|
|
825
|
+
movePath(src, dst);
|
|
813
826
|
logger.success(`✔ installed ${pkg}`);
|
|
814
827
|
const pkgJsonFile = path.join(dst, 'package.json');
|
|
815
828
|
if (!fs.existsSync(pkgJsonFile)) {
|
|
@@ -825,8 +838,9 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
825
838
|
}
|
|
826
839
|
}
|
|
827
840
|
finally {
|
|
828
|
-
|
|
841
|
+
// chdir out first: Windows locks the process working directory
|
|
829
842
|
process.chdir(originalDir);
|
|
843
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
830
844
|
}
|
|
831
845
|
}
|
|
832
846
|
if (inModule && pkgData && pkgJsonPath) {
|
|
@@ -946,7 +960,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
946
960
|
}
|
|
947
961
|
const pkgData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
948
962
|
const dependencies = pkgData.dependencies || {};
|
|
949
|
-
const skitchExtDir =
|
|
963
|
+
const skitchExtDir = this.getExtensionsPath();
|
|
950
964
|
const installed = [];
|
|
951
965
|
const installedVersions = {};
|
|
952
966
|
for (const [name, version] of Object.entries(dependencies)) {
|
|
@@ -969,7 +983,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
969
983
|
*/
|
|
970
984
|
getWorkspaceInstalledModules() {
|
|
971
985
|
this.ensureWorkspace();
|
|
972
|
-
const extensionsDir =
|
|
986
|
+
const extensionsDir = this.getExtensionsPath();
|
|
973
987
|
if (!fs.existsSync(extensionsDir)) {
|
|
974
988
|
return [];
|
|
975
989
|
}
|
|
@@ -1620,7 +1634,8 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1620
1634
|
if (base !== expected)
|
|
1621
1635
|
issues.push({ code: 'control_filename_mismatch', message: `Control filename ${base} != ${expected}`, file: controlPath });
|
|
1622
1636
|
}
|
|
1623
|
-
|
|
1637
|
+
const reported = issues.map(issue => ({ ...issue, file: toPosixPath(issue.file) }));
|
|
1638
|
+
return { ok: reported.length === 0, name: info.extname, path: toPosixPath(modPath), issues: reported };
|
|
1624
1639
|
}
|
|
1625
1640
|
renameModule(newName, opts) {
|
|
1626
1641
|
this.ensureModule();
|
package/esm/projects/deploy.js
CHANGED
|
@@ -23,7 +23,7 @@ export const deployProject = async (opts, name, database, pkg, toChange) => {
|
|
|
23
23
|
throw errors.MODULE_NOT_FOUND({ name });
|
|
24
24
|
}
|
|
25
25
|
const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
|
|
26
|
-
const moduleProject = new PgpmPackage(modulePath);
|
|
26
|
+
const moduleProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
27
27
|
log.info(`📦 Resolving dependencies for ${name}...`);
|
|
28
28
|
const extensions = moduleProject.getModuleExtensions();
|
|
29
29
|
const pgPool = getPgPool({ ...opts.pg, database });
|
|
@@ -42,7 +42,7 @@ export const deployProject = async (opts, name, database, pkg, toChange) => {
|
|
|
42
42
|
log.debug(`→ Path: ${modulePath}`);
|
|
43
43
|
if (mergedOpts.deployment.fast) {
|
|
44
44
|
// Use fast deployment strategy
|
|
45
|
-
const localProject = new PgpmPackage(modulePath);
|
|
45
|
+
const localProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
46
46
|
const cacheKey = getCacheKey(mergedOpts.pg, extension, database);
|
|
47
47
|
if (mergedOpts.deployment.cache && deployFastCache[cacheKey]) {
|
|
48
48
|
log.warn(`⚡ Using cached package for ${extension}.`);
|
package/esm/projects/revert.js
CHANGED
|
@@ -14,7 +14,7 @@ export const revertProject = async (opts, name, database, pkg, options) => {
|
|
|
14
14
|
throw errors.MODULE_NOT_FOUND({ name });
|
|
15
15
|
}
|
|
16
16
|
const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
|
|
17
|
-
const moduleProject = new PgpmPackage(modulePath);
|
|
17
|
+
const moduleProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
18
18
|
log.info(`📦 Resolving dependencies for ${name}...`);
|
|
19
19
|
const extensions = moduleProject.getModuleExtensions();
|
|
20
20
|
const pgPool = getPgPool({
|
package/esm/projects/verify.js
CHANGED
|
@@ -14,7 +14,7 @@ export const verifyProject = async (opts, name, database, pkg, options) => {
|
|
|
14
14
|
throw errors.MODULE_NOT_FOUND({ name });
|
|
15
15
|
}
|
|
16
16
|
const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
|
|
17
|
-
const moduleProject = new PgpmPackage(modulePath);
|
|
17
|
+
const moduleProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
18
18
|
log.info(`📦 Resolving dependencies for ${name}...`);
|
|
19
19
|
const extensions = moduleProject.getModuleExtensions();
|
|
20
20
|
const pgPool = getPgPool({
|
package/esm/resolution/deps.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { sync as glob } from 'glob';
|
|
3
3
|
import { join, relative } from 'path';
|
|
4
4
|
import { PgpmPackage } from '../core/class/pgpm';
|
|
5
|
+
import { globPattern, toPosixPath } from '../utils/glob';
|
|
5
6
|
import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
|
|
6
7
|
import { scanDeployScript } from '@pgpmjs/ast/files/sql/header';
|
|
7
8
|
import { errors } from '@pgpmjs/types';
|
|
@@ -331,10 +332,10 @@ export const resolveDependencies = (packageDir, extname, options = {}) => {
|
|
|
331
332
|
const deps = {};
|
|
332
333
|
const tagMappings = {};
|
|
333
334
|
// Process SQL files and build dependency graph
|
|
334
|
-
const files = glob(
|
|
335
|
+
const files = glob(globPattern(packageDir, 'deploy/**/*.sql')).sort((a, b) => a.localeCompare(b));
|
|
335
336
|
for (const file of files) {
|
|
336
337
|
const data = readFileSync(file, 'utf-8');
|
|
337
|
-
const key = '/' + relative(packageDir, file);
|
|
338
|
+
const key = '/' + toPosixPath(relative(packageDir, file));
|
|
338
339
|
deps[key] = [];
|
|
339
340
|
const { requires } = scanDeployScript(data, { key, extname, makeKey });
|
|
340
341
|
for (const dep of requires) {
|
package/esm/utils/fs.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* Move a file or directory, falling back to copy + remove across devices.
|
|
4
|
+
* Replaces shelling out to `mv`, which does not exist on Windows.
|
|
5
|
+
*/
|
|
6
|
+
export const movePath = (src, dst) => {
|
|
7
|
+
try {
|
|
8
|
+
fs.renameSync(src, dst);
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
if (e?.code !== 'EXDEV' && e?.code !== 'EPERM')
|
|
12
|
+
throw e;
|
|
13
|
+
fs.cpSync(src, dst, { recursive: true });
|
|
14
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { sync as globSync } from 'glob';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Convert a native filesystem path to a posix-style path.
|
|
5
|
+
* Windows path separators are not valid inside glob patterns.
|
|
6
|
+
*/
|
|
7
|
+
export const toPosixPath = (p) => p.replace(/\\/g, '/');
|
|
8
|
+
/**
|
|
9
|
+
* Join path segments into a glob pattern.
|
|
10
|
+
*
|
|
11
|
+
* `glob` treats `\` as an escape character on every platform, so a Windows
|
|
12
|
+
* path produced by `path.join` silently matches nothing. Patterns must always
|
|
13
|
+
* use forward slashes, which Windows accepts as a separator.
|
|
14
|
+
*/
|
|
15
|
+
export const globPattern = (...segments) => toPosixPath(path.join(...segments));
|
|
16
|
+
/**
|
|
17
|
+
* Cross-platform `glob.sync` over path segments joined into a pattern.
|
|
18
|
+
*/
|
|
19
|
+
export const globPaths = (...segments) => globSync(globPattern(...segments));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM Package and Migration Tools",
|
|
6
6
|
"main": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "https://github.com/constructive-io/constructive/issues"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"copy": "npm run copy:pkg
|
|
23
|
+
"copy": "npm run copy:pkg && npm run copy:sql",
|
|
24
24
|
"copy:pkg": "makage assets",
|
|
25
25
|
"copy:sql": "copyfiles -f src/migrate/sql/* dist/migrate/sql && copyfiles -f src/migrate/sql/* dist/esm/migrate/sql",
|
|
26
26
|
"clean": "makage clean",
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
"makage": "^0.3.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@pgpmjs/ast": "^0.1.
|
|
52
|
-
"@pgpmjs/bundle": "^0.
|
|
53
|
-
"@pgpmjs/env": "^2.
|
|
51
|
+
"@pgpmjs/ast": "^0.1.2",
|
|
52
|
+
"@pgpmjs/bundle": "^0.2.1",
|
|
53
|
+
"@pgpmjs/env": "^2.29.0",
|
|
54
54
|
"@pgpmjs/logger": "^2.14.0",
|
|
55
|
-
"@pgpmjs/server-utils": "^3.15.
|
|
56
|
-
"@pgpmjs/types": "^2.
|
|
57
|
-
"csv-to-pg": "^4.0.
|
|
55
|
+
"@pgpmjs/server-utils": "^3.15.4",
|
|
56
|
+
"@pgpmjs/types": "^2.36.0",
|
|
57
|
+
"csv-to-pg": "^4.0.1",
|
|
58
58
|
"genomic": "^5.6.2",
|
|
59
59
|
"glob": "^13.0.6",
|
|
60
60
|
"minimatch": "^10.2.5",
|
|
61
61
|
"parse-package-name": "^1.0.0",
|
|
62
62
|
"pg": "^8.21.0",
|
|
63
|
-
"pg-cache": "^3.15.
|
|
63
|
+
"pg-cache": "^3.15.3",
|
|
64
64
|
"pg-env": "^1.18.1",
|
|
65
65
|
"pgsql-deparser": "^18.1.1",
|
|
66
66
|
"pgsql-parser": "^18.1.1",
|
|
67
67
|
"yanse": "^0.2.1"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "39db6d576bce7bc1de8123849f237b1695779d88"
|
|
70
70
|
}
|
package/projects/deploy.js
CHANGED
|
@@ -59,7 +59,7 @@ const deployProject = async (opts, name, database, pkg, toChange) => {
|
|
|
59
59
|
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
60
60
|
}
|
|
61
61
|
const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
|
|
62
|
-
const moduleProject = new pgpm_1.PgpmPackage(modulePath);
|
|
62
|
+
const moduleProject = new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
63
63
|
log.info(`📦 Resolving dependencies for ${name}...`);
|
|
64
64
|
const extensions = moduleProject.getModuleExtensions();
|
|
65
65
|
const pgPool = (0, pg_cache_1.getPgPool)({ ...opts.pg, database });
|
|
@@ -78,7 +78,7 @@ const deployProject = async (opts, name, database, pkg, toChange) => {
|
|
|
78
78
|
log.debug(`→ Path: ${modulePath}`);
|
|
79
79
|
if (mergedOpts.deployment.fast) {
|
|
80
80
|
// Use fast deployment strategy
|
|
81
|
-
const localProject = new pgpm_1.PgpmPackage(modulePath);
|
|
81
|
+
const localProject = new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
82
82
|
const cacheKey = getCacheKey(mergedOpts.pg, extension, database);
|
|
83
83
|
if (mergedOpts.deployment.cache && deployFastCache[cacheKey]) {
|
|
84
84
|
log.warn(`⚡ Using cached package for ${extension}.`);
|
package/projects/revert.js
CHANGED
|
@@ -50,7 +50,7 @@ const revertProject = async (opts, name, database, pkg, options) => {
|
|
|
50
50
|
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
51
51
|
}
|
|
52
52
|
const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
|
|
53
|
-
const moduleProject = new pgpm_1.PgpmPackage(modulePath);
|
|
53
|
+
const moduleProject = new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
54
54
|
log.info(`📦 Resolving dependencies for ${name}...`);
|
|
55
55
|
const extensions = moduleProject.getModuleExtensions();
|
|
56
56
|
const pgPool = (0, pg_cache_1.getPgPool)({
|
package/projects/verify.js
CHANGED
|
@@ -50,7 +50,7 @@ const verifyProject = async (opts, name, database, pkg, options) => {
|
|
|
50
50
|
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
51
51
|
}
|
|
52
52
|
const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
|
|
53
|
-
const moduleProject = new pgpm_1.PgpmPackage(modulePath);
|
|
53
|
+
const moduleProject = new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
|
|
54
54
|
log.info(`📦 Resolving dependencies for ${name}...`);
|
|
55
55
|
const extensions = moduleProject.getModuleExtensions();
|
|
56
56
|
const pgPool = (0, pg_cache_1.getPgPool)({
|
package/resolution/deps.js
CHANGED
|
@@ -5,6 +5,7 @@ const fs_1 = require("fs");
|
|
|
5
5
|
const glob_1 = require("glob");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const pgpm_1 = require("../core/class/pgpm");
|
|
8
|
+
const glob_2 = require("../utils/glob");
|
|
8
9
|
const parser_1 = require("@pgpmjs/ast/files/plan/parser");
|
|
9
10
|
const header_1 = require("@pgpmjs/ast/files/sql/header");
|
|
10
11
|
const types_1 = require("@pgpmjs/types");
|
|
@@ -335,10 +336,10 @@ const resolveDependencies = (packageDir, extname, options = {}) => {
|
|
|
335
336
|
const deps = {};
|
|
336
337
|
const tagMappings = {};
|
|
337
338
|
// Process SQL files and build dependency graph
|
|
338
|
-
const files = (0, glob_1.sync)(
|
|
339
|
+
const files = (0, glob_1.sync)((0, glob_2.globPattern)(packageDir, 'deploy/**/*.sql')).sort((a, b) => a.localeCompare(b));
|
|
339
340
|
for (const file of files) {
|
|
340
341
|
const data = (0, fs_1.readFileSync)(file, 'utf-8');
|
|
341
|
-
const key = '/' + (0, path_1.relative)(packageDir, file);
|
|
342
|
+
const key = '/' + (0, glob_2.toPosixPath)((0, path_1.relative)(packageDir, file));
|
|
342
343
|
deps[key] = [];
|
|
343
344
|
const { requires } = (0, header_1.scanDeployScript)(data, { key, extname, makeKey });
|
|
344
345
|
for (const dep of requires) {
|
package/utils/fs.d.ts
ADDED
package/utils/fs.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.movePath = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
/**
|
|
9
|
+
* Move a file or directory, falling back to copy + remove across devices.
|
|
10
|
+
* Replaces shelling out to `mv`, which does not exist on Windows.
|
|
11
|
+
*/
|
|
12
|
+
const movePath = (src, dst) => {
|
|
13
|
+
try {
|
|
14
|
+
fs_1.default.renameSync(src, dst);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
if (e?.code !== 'EXDEV' && e?.code !== 'EPERM')
|
|
18
|
+
throw e;
|
|
19
|
+
fs_1.default.cpSync(src, dst, { recursive: true });
|
|
20
|
+
fs_1.default.rmSync(src, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
exports.movePath = movePath;
|
package/utils/glob.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a native filesystem path to a posix-style path.
|
|
3
|
+
* Windows path separators are not valid inside glob patterns.
|
|
4
|
+
*/
|
|
5
|
+
export declare const toPosixPath: (p: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Join path segments into a glob pattern.
|
|
8
|
+
*
|
|
9
|
+
* `glob` treats `\` as an escape character on every platform, so a Windows
|
|
10
|
+
* path produced by `path.join` silently matches nothing. Patterns must always
|
|
11
|
+
* use forward slashes, which Windows accepts as a separator.
|
|
12
|
+
*/
|
|
13
|
+
export declare const globPattern: (...segments: string[]) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Cross-platform `glob.sync` over path segments joined into a pattern.
|
|
16
|
+
*/
|
|
17
|
+
export declare const globPaths: (...segments: string[]) => string[];
|
package/utils/glob.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.globPaths = exports.globPattern = exports.toPosixPath = void 0;
|
|
7
|
+
const glob_1 = require("glob");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Convert a native filesystem path to a posix-style path.
|
|
11
|
+
* Windows path separators are not valid inside glob patterns.
|
|
12
|
+
*/
|
|
13
|
+
const toPosixPath = (p) => p.replace(/\\/g, '/');
|
|
14
|
+
exports.toPosixPath = toPosixPath;
|
|
15
|
+
/**
|
|
16
|
+
* Join path segments into a glob pattern.
|
|
17
|
+
*
|
|
18
|
+
* `glob` treats `\` as an escape character on every platform, so a Windows
|
|
19
|
+
* path produced by `path.join` silently matches nothing. Patterns must always
|
|
20
|
+
* use forward slashes, which Windows accepts as a separator.
|
|
21
|
+
*/
|
|
22
|
+
const globPattern = (...segments) => (0, exports.toPosixPath)(path_1.default.join(...segments));
|
|
23
|
+
exports.globPattern = globPattern;
|
|
24
|
+
/**
|
|
25
|
+
* Cross-platform `glob.sync` over path segments joined into a pattern.
|
|
26
|
+
*/
|
|
27
|
+
const globPaths = (...segments) => (0, glob_1.sync)((0, exports.globPattern)(...segments));
|
|
28
|
+
exports.globPaths = globPaths;
|