@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 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
@@ -10,3 +10,4 @@
10
10
  */
11
11
  export * from '@pgpmjs/bundle';
12
12
  export * from './apply';
13
+ export * from './apply-envelope';
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);
@@ -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;
@@ -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 => glob.sync(path_1.default.join(this.workspacePath, 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
- ...glob.sync(path_1.default.join(this.workspacePath, EXTENSIONS_DIR, '{*,@*/*}'))
258
+ ...(0, glob_1.globPaths)(this.workspacePath, this.extensionsDir, '{*,@*/*}')
246
259
  ];
247
- const moduleFiles = [...new Set(packageDirs.flatMap(dir => glob.sync(`${dir}/**/*.control`)))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
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 = path_1.default.join(this.workspacePath, EXTENSIONS_DIR);
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 ./extensions`, {
844
+ (0, child_process_1.execSync)(`npm install ${pkgstr} --production --prefix ./${this.extensionsDir}`, {
832
845
  stdio: 'inherit'
833
846
  });
834
- const matches = glob.sync('./extensions/**/pgpm.plan');
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, child_process_1.execSync)(`mv "${src}" "${dst}"`);
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
- fs_1.default.rmSync(tempDir, { recursive: true, force: true });
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 = path_1.default.join(this.workspacePath, EXTENSIONS_DIR);
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 = path_1.default.join(this.workspacePath, EXTENSIONS_DIR);
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
- return { ok: issues.length === 0, name: info.extname, path: modPath, issues };
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
+ }
@@ -13,3 +13,5 @@
13
13
  export * from '@pgpmjs/bundle';
14
14
  // applyBundle stays in core: it drives PgpmMigrate (the deploy engine).
15
15
  export * from './apply';
16
+ // applyEnvelope: schema apply + data/fixtures part replay for a BundleEnvelope.
17
+ export * from './apply-envelope';
@@ -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 => glob.sync(path.join(this.workspacePath, 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
- ...glob.sync(path.join(this.workspacePath, EXTENSIONS_DIR, '{*,@*/*}'))
219
+ ...globPaths(this.workspacePath, this.extensionsDir, '{*,@*/*}')
207
220
  ];
208
- const moduleFiles = [...new Set(packageDirs.flatMap(dir => glob.sync(`${dir}/**/*.control`)))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
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 = path.join(this.workspacePath, EXTENSIONS_DIR);
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 ./extensions`, {
805
+ execSync(`npm install ${pkgstr} --production --prefix ./${this.extensionsDir}`, {
793
806
  stdio: 'inherit'
794
807
  });
795
- const matches = glob.sync('./extensions/**/pgpm.plan');
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
- execSync(`mv "${src}" "${dst}"`);
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
- fs.rmSync(tempDir, { recursive: true, force: true });
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 = path.join(this.workspacePath, EXTENSIONS_DIR);
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 = path.join(this.workspacePath, EXTENSIONS_DIR);
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
- return { ok: issues.length === 0, name: info.extname, path: modPath, issues };
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();
@@ -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}.`);
@@ -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({
@@ -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({
@@ -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(`${packageDir}/deploy/**/*.sql`).sort((a, b) => a.localeCompare(b));
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) {
@@ -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.2.1",
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; npm run copy:sql",
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.1",
52
- "@pgpmjs/bundle": "^0.1.1",
53
- "@pgpmjs/env": "^2.28.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.3",
56
- "@pgpmjs/types": "^2.35.1",
57
- "csv-to-pg": "^4.0.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.2",
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": "51c3e15e9d509154d0a8102bb18f6e39cfa4e0c6"
69
+ "gitHead": "39db6d576bce7bc1de8123849f237b1695779d88"
70
70
  }
@@ -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}.`);
@@ -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)({
@@ -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)({
@@ -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)(`${packageDir}/deploy/**/*.sql`).sort((a, b) => a.localeCompare(b));
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
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Move a file or directory, falling back to copy + remove across devices.
3
+ * Replaces shelling out to `mv`, which does not exist on Windows.
4
+ */
5
+ export declare const movePath: (src: string, dst: string) => void;
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;
@@ -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;