@pgpmjs/core 7.0.0 → 7.2.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/bundle/apply.d.ts +93 -0
- package/bundle/apply.js +137 -0
- package/bundle/index.d.ts +12 -0
- package/bundle/index.js +31 -0
- package/core/class/pgpm.d.ts +2 -2
- package/core/class/pgpm.js +6 -6
- package/esm/bundle/apply.js +131 -0
- package/esm/bundle/index.js +15 -0
- package/esm/core/class/pgpm.js +6 -6
- package/esm/index.js +7 -3
- package/esm/migrate/client.js +1 -1
- package/esm/migrate/utils/hash.js +3 -6
- package/esm/modules/modules.js +1 -1
- package/esm/packaging/package.js +40 -24
- package/esm/rebundle/index.js +4 -0
- package/esm/rebundle/module.js +111 -0
- package/esm/rebundle/rebundle.js +170 -0
- package/esm/rebundle/workspace.js +170 -0
- package/esm/refactor/rename.js +5 -27
- package/esm/resolution/deps.js +2 -2
- package/esm/resolution/resolve.js +2 -2
- package/esm/slice/slice.js +1 -1
- package/esm/workspace/minimal.js +73 -0
- package/index.d.ts +6 -2
- package/index.js +8 -4
- package/migrate/client.js +1 -1
- package/migrate/utils/hash.d.ts +2 -4
- package/migrate/utils/hash.js +4 -8
- package/modules/modules.d.ts +1 -1
- package/modules/modules.js +1 -1
- package/package.json +4 -2
- package/packaging/package.d.ts +23 -1
- package/packaging/package.js +42 -25
- package/rebundle/index.d.ts +4 -0
- package/{files/sql-scripts → rebundle}/index.js +4 -2
- package/rebundle/module.d.ts +23 -0
- package/rebundle/module.js +116 -0
- package/rebundle/rebundle.d.ts +26 -0
- package/rebundle/rebundle.js +175 -0
- package/rebundle/types.d.ts +185 -0
- package/rebundle/workspace.d.ts +15 -0
- package/rebundle/workspace.js +173 -0
- package/refactor/rename.d.ts +2 -8
- package/refactor/rename.js +6 -29
- package/resolution/deps.d.ts +1 -1
- package/resolution/deps.js +2 -2
- package/resolution/resolve.js +2 -2
- package/slice/slice.d.ts +1 -1
- package/slice/slice.js +1 -1
- package/slice/types.d.ts +1 -1
- package/workspace/minimal.d.ts +64 -0
- package/workspace/minimal.js +82 -0
- package/esm/files/extension/index.js +0 -3
- package/esm/files/extension/reader.js +0 -79
- package/esm/files/extension/writer.js +0 -99
- package/esm/files/index.js +0 -6
- package/esm/files/plan/generator.js +0 -82
- package/esm/files/plan/index.js +0 -5
- package/esm/files/plan/parser.js +0 -297
- package/esm/files/plan/validators.js +0 -181
- package/esm/files/plan/writer.js +0 -142
- package/esm/files/sql/header.js +0 -259
- package/esm/files/sql/index.js +0 -2
- package/esm/files/sql/writer.js +0 -107
- package/esm/files/sql-scripts/index.js +0 -2
- package/esm/files/sql-scripts/reader.js +0 -19
- package/esm/files/types/index.js +0 -1
- package/esm/utils/author.js +0 -11
- package/files/extension/index.d.ts +0 -2
- package/files/extension/index.js +0 -19
- package/files/extension/reader.d.ts +0 -24
- package/files/extension/reader.js +0 -86
- package/files/extension/writer.d.ts +0 -48
- package/files/extension/writer.js +0 -107
- package/files/index.d.ts +0 -5
- package/files/index.js +0 -22
- package/files/plan/generator.d.ts +0 -23
- package/files/plan/generator.js +0 -90
- package/files/plan/index.d.ts +0 -4
- package/files/plan/index.js +0 -21
- package/files/plan/parser.d.ts +0 -28
- package/files/plan/parser.js +0 -304
- package/files/plan/validators.d.ts +0 -52
- package/files/plan/validators.js +0 -187
- package/files/plan/writer.d.ts +0 -27
- package/files/plan/writer.js +0 -152
- package/files/sql/header.d.ts +0 -114
- package/files/sql/header.js +0 -266
- package/files/sql/index.d.ts +0 -2
- package/files/sql/index.js +0 -18
- package/files/sql/writer.d.ts +0 -12
- package/files/sql/writer.js +0 -114
- package/files/sql-scripts/index.d.ts +0 -1
- package/files/sql-scripts/reader.d.ts +0 -8
- package/files/sql-scripts/reader.js +0 -23
- package/files/types/index.d.ts +0 -46
- package/files/types/index.js +0 -17
- package/files/types/package.d.ts +0 -20
- package/utils/author.d.ts +0 -5
- package/utils/author.js +0 -14
- /package/esm/{files/types/package.js → rebundle/types.js} +0 -0
- /package/{files/types/package.js → rebundle/types.js} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PgConfig } from 'pg-env';
|
|
2
|
+
import { PgpmScriptKind } from '@pgpmjs/ast/module/types';
|
|
3
|
+
import { BundleIssue, MigrationBundle } from '@pgpmjs/bundle';
|
|
4
|
+
import { PgpmMigrateOptions } from '../migrate/client';
|
|
5
|
+
import { DeployResult, VerifyResult } from '../migrate/types';
|
|
6
|
+
/** A script whose SQL references something outside the allowed namespace. */
|
|
7
|
+
export interface NamespaceViolation {
|
|
8
|
+
change: string;
|
|
9
|
+
kind: PgpmScriptKind;
|
|
10
|
+
/** Offending references reported by the caller's validator. */
|
|
11
|
+
references: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A validator for transpiled bundles: given a script's SQL, return the
|
|
15
|
+
* references that fall *outside* the target namespace (empty = clean). Core
|
|
16
|
+
* stays parser-agnostic; the caller (constructive-db) supplies the AST check.
|
|
17
|
+
*/
|
|
18
|
+
export type ReferenceValidator = (sql: string, ctx: {
|
|
19
|
+
change: string;
|
|
20
|
+
kind: PgpmScriptKind;
|
|
21
|
+
}) => string[];
|
|
22
|
+
/** Pre-flight analysis of an apply, computed without touching the database. */
|
|
23
|
+
export interface ApplyBundlePreview {
|
|
24
|
+
name: string;
|
|
25
|
+
/** Changes in deploy order. */
|
|
26
|
+
deployOrder: string[];
|
|
27
|
+
/** Digest/order/integrity problems from {@link verifyBundle}. */
|
|
28
|
+
bundleIssues: BundleIssue[];
|
|
29
|
+
/** References outside the target namespace (only when a validator is given). */
|
|
30
|
+
namespaceViolations: NamespaceViolation[];
|
|
31
|
+
/** Reverse deploy order — the revert sequence that fully undoes this apply. */
|
|
32
|
+
rollbackPlan: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface PreviewBundleApplyOptions {
|
|
35
|
+
validateReferences?: ReferenceValidator;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Analyze a bundle apply without executing it: verify artifact integrity, run
|
|
39
|
+
* the optional namespace-reference validator, and derive the rollback plan.
|
|
40
|
+
* Pure — no database, no disk.
|
|
41
|
+
*/
|
|
42
|
+
export declare function previewBundleApply(bundle: MigrationBundle, options?: PreviewBundleApplyOptions): ApplyBundlePreview;
|
|
43
|
+
export interface ApplyBundleOptions extends PreviewBundleApplyOptions {
|
|
44
|
+
/** Target database. */
|
|
45
|
+
config: PgConfig;
|
|
46
|
+
/** Analyze only; never touch the database. Default false. */
|
|
47
|
+
dryRun?: boolean;
|
|
48
|
+
/** Run `verify` after a successful deploy. Default false. */
|
|
49
|
+
verify?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Wall-clock budget for the deploy (+ optional verify). On expiry the result
|
|
52
|
+
* is flagged `timedOut` and a {@link BundleApplyError} is thrown. The apply
|
|
53
|
+
* runs in a single transaction (see `useTransaction`), so the database rolls
|
|
54
|
+
* back rather than being left half-applied.
|
|
55
|
+
*/
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
/** Apply as one atomic transaction so any failure rolls back. Default true. */
|
|
58
|
+
useTransaction?: boolean;
|
|
59
|
+
/** Directory to materialize into (default: a temp dir, removed afterward). */
|
|
60
|
+
workDir?: string;
|
|
61
|
+
migrateOptions?: PgpmMigrateOptions;
|
|
62
|
+
/** Apply even if artifact verification fails. Default false (refuse). */
|
|
63
|
+
allowUnverified?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/** Explicit outcome of an {@link applyBundle} call. */
|
|
66
|
+
export interface ApplyBundleResult {
|
|
67
|
+
preview: ApplyBundlePreview;
|
|
68
|
+
dryRun: boolean;
|
|
69
|
+
/** True when the deploy actually ran against the database. */
|
|
70
|
+
executed: boolean;
|
|
71
|
+
deploy?: DeployResult;
|
|
72
|
+
verify?: VerifyResult;
|
|
73
|
+
durationMs: number;
|
|
74
|
+
timedOut: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** Thrown when an apply is refused pre-flight or exceeds its timeout. */
|
|
77
|
+
export declare class BundleApplyError extends Error {
|
|
78
|
+
readonly preview: ApplyBundlePreview;
|
|
79
|
+
readonly timedOut: boolean;
|
|
80
|
+
constructor(message: string, preview: ApplyBundlePreview, timedOut?: boolean);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Safely apply a {@link MigrationBundle} to a target database.
|
|
84
|
+
*
|
|
85
|
+
* A thin, safety-first wrapper over {@link PgpmMigrate}: it gates on artifact
|
|
86
|
+
* integrity and (optionally) namespace validation, supports a dry run, applies
|
|
87
|
+
* atomically in a single transaction, enforces a timeout, and returns an
|
|
88
|
+
* explicit result including the rollback plan.
|
|
89
|
+
*
|
|
90
|
+
* The mechanics (materialize → deploy → verify) reuse existing plumbing;
|
|
91
|
+
* nothing here re-implements deployment.
|
|
92
|
+
*/
|
|
93
|
+
export declare function applyBundle(bundle: MigrationBundle, options: ApplyBundleOptions): Promise<ApplyBundleResult>;
|
package/bundle/apply.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BundleApplyError = void 0;
|
|
4
|
+
exports.previewBundleApply = previewBundleApply;
|
|
5
|
+
exports.applyBundle = applyBundle;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const bundle_1 = require("@pgpmjs/bundle");
|
|
10
|
+
const client_1 = require("../migrate/client");
|
|
11
|
+
/**
|
|
12
|
+
* Analyze a bundle apply without executing it: verify artifact integrity, run
|
|
13
|
+
* the optional namespace-reference validator, and derive the rollback plan.
|
|
14
|
+
* Pure — no database, no disk.
|
|
15
|
+
*/
|
|
16
|
+
function previewBundleApply(bundle, options = {}) {
|
|
17
|
+
const namespaceViolations = [];
|
|
18
|
+
if (options.validateReferences) {
|
|
19
|
+
for (const change of bundle.changes) {
|
|
20
|
+
for (const script of [change.deploy, change.revert, change.verify]) {
|
|
21
|
+
if (!script)
|
|
22
|
+
continue;
|
|
23
|
+
const references = options.validateReferences(script.sql, {
|
|
24
|
+
change: change.name,
|
|
25
|
+
kind: script.kind
|
|
26
|
+
});
|
|
27
|
+
if (references.length > 0) {
|
|
28
|
+
namespaceViolations.push({ change: change.name, kind: script.kind, references });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name: bundle.manifest.name,
|
|
35
|
+
deployOrder: [...bundle.manifest.deployOrder],
|
|
36
|
+
bundleIssues: (0, bundle_1.verifyBundle)(bundle),
|
|
37
|
+
namespaceViolations,
|
|
38
|
+
rollbackPlan: [...bundle.manifest.deployOrder].reverse()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Thrown when an apply is refused pre-flight or exceeds its timeout. */
|
|
42
|
+
class BundleApplyError extends Error {
|
|
43
|
+
preview;
|
|
44
|
+
timedOut;
|
|
45
|
+
constructor(message, preview, timedOut = false) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.preview = preview;
|
|
48
|
+
this.timedOut = timedOut;
|
|
49
|
+
this.name = 'BundleApplyError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.BundleApplyError = BundleApplyError;
|
|
53
|
+
const TIMEOUT = Symbol('timeout');
|
|
54
|
+
async function withTimeout(work, ms) {
|
|
55
|
+
if (!ms)
|
|
56
|
+
return work;
|
|
57
|
+
let timer;
|
|
58
|
+
const timeout = new Promise(resolve => {
|
|
59
|
+
timer = setTimeout(() => resolve(TIMEOUT), ms);
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
return await Promise.race([work, timeout]);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Safely apply a {@link MigrationBundle} to a target database.
|
|
70
|
+
*
|
|
71
|
+
* A thin, safety-first wrapper over {@link PgpmMigrate}: it gates on artifact
|
|
72
|
+
* integrity and (optionally) namespace validation, supports a dry run, applies
|
|
73
|
+
* atomically in a single transaction, enforces a timeout, and returns an
|
|
74
|
+
* explicit result including the rollback plan.
|
|
75
|
+
*
|
|
76
|
+
* The mechanics (materialize → deploy → verify) reuse existing plumbing;
|
|
77
|
+
* nothing here re-implements deployment.
|
|
78
|
+
*/
|
|
79
|
+
async function applyBundle(bundle, options) {
|
|
80
|
+
const started = Date.now();
|
|
81
|
+
const preview = previewBundleApply(bundle, options);
|
|
82
|
+
if (!options.allowUnverified && preview.bundleIssues.length > 0) {
|
|
83
|
+
throw new BundleApplyError(`Refusing to apply "${preview.name}": bundle failed integrity verification ` +
|
|
84
|
+
`(${preview.bundleIssues.length} issue(s): ${preview.bundleIssues.map(i => i.kind).join(', ')})`, preview);
|
|
85
|
+
}
|
|
86
|
+
if (preview.namespaceViolations.length > 0) {
|
|
87
|
+
throw new BundleApplyError(`Refusing to apply "${preview.name}": ${preview.namespaceViolations.length} ` +
|
|
88
|
+
`reference(s) fall outside the target namespace`, preview);
|
|
89
|
+
}
|
|
90
|
+
if (options.dryRun) {
|
|
91
|
+
return {
|
|
92
|
+
preview,
|
|
93
|
+
dryRun: true,
|
|
94
|
+
executed: false,
|
|
95
|
+
durationMs: Date.now() - started,
|
|
96
|
+
timedOut: false
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const ownWorkDir = options.workDir === undefined;
|
|
100
|
+
const workDir = options.workDir ?? (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'pgpm-apply-'));
|
|
101
|
+
(0, bundle_1.materializeBundle)(bundle, workDir);
|
|
102
|
+
const migrate = new client_1.PgpmMigrate(options.config, options.migrateOptions);
|
|
103
|
+
const useTransaction = options.useTransaction ?? true;
|
|
104
|
+
try {
|
|
105
|
+
const run = (async () => {
|
|
106
|
+
const deploy = await migrate.deploy({ modulePath: workDir, useTransaction });
|
|
107
|
+
let verify;
|
|
108
|
+
if (options.verify) {
|
|
109
|
+
verify = await migrate.verify({ modulePath: workDir });
|
|
110
|
+
}
|
|
111
|
+
return { deploy, verify };
|
|
112
|
+
})();
|
|
113
|
+
const outcome = await withTimeout(run, options.timeoutMs);
|
|
114
|
+
if (outcome === TIMEOUT) {
|
|
115
|
+
throw new BundleApplyError(`Apply of "${preview.name}" exceeded ${options.timeoutMs}ms`, preview, true);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
preview,
|
|
119
|
+
dryRun: false,
|
|
120
|
+
executed: true,
|
|
121
|
+
deploy: outcome.deploy,
|
|
122
|
+
verify: outcome.verify,
|
|
123
|
+
durationMs: Date.now() - started,
|
|
124
|
+
timedOut: false
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
if (ownWorkDir) {
|
|
129
|
+
try {
|
|
130
|
+
(0, fs_1.rmSync)(workDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// best-effort cleanup
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgpm migration bundles: the portable, content-addressed artifact layer.
|
|
3
|
+
*
|
|
4
|
+
* A {@link MigrationBundle} is a deterministic snapshot of a pgpm module — plan,
|
|
5
|
+
* control, ordered changes, and per-change/whole-bundle sha256 digests — with an
|
|
6
|
+
* optional provenance manifest. It is the substrate for the `exportMigrationBundle`
|
|
7
|
+
* / `applyMigrationBundle` / `transpileMigrationBundle` cloud functions: build one
|
|
8
|
+
* from a module AST, transport or transform it, verify its digests, then
|
|
9
|
+
* materialize it back into a deployable module.
|
|
10
|
+
*/
|
|
11
|
+
export * from '@pgpmjs/bundle';
|
|
12
|
+
export * from './apply';
|
package/bundle/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* pgpm migration bundles: the portable, content-addressed artifact layer.
|
|
19
|
+
*
|
|
20
|
+
* A {@link MigrationBundle} is a deterministic snapshot of a pgpm module — plan,
|
|
21
|
+
* control, ordered changes, and per-change/whole-bundle sha256 digests — with an
|
|
22
|
+
* optional provenance manifest. It is the substrate for the `exportMigrationBundle`
|
|
23
|
+
* / `applyMigrationBundle` / `transpileMigrationBundle` cloud functions: build one
|
|
24
|
+
* from a module AST, transport or transform it, verify its digests, then
|
|
25
|
+
* materialize it back into a deployable module.
|
|
26
|
+
*/
|
|
27
|
+
// Pure artifact layer moved to @pgpmjs/bundle; re-exported here so existing
|
|
28
|
+
// `@pgpmjs/core` consumers keep importing the whole bundle surface unchanged.
|
|
29
|
+
__exportStar(require("@pgpmjs/bundle"), exports);
|
|
30
|
+
// applyBundle stays in core: it drives PgpmMigrate (the deploy engine).
|
|
31
|
+
__exportStar(require("./apply"), exports);
|
package/core/class/pgpm.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PgpmOptions, PgpmWorkspaceConfig } from '@pgpmjs/types';
|
|
2
2
|
import { PgConfig } from 'pg-env';
|
|
3
|
-
import { ExtensionInfo } from '
|
|
4
|
-
import { PackageAnalysisResult, RenameOptions } from '
|
|
3
|
+
import { ExtensionInfo } from '@pgpmjs/ast/files';
|
|
4
|
+
import { PackageAnalysisResult, RenameOptions } from '@pgpmjs/ast/files/types';
|
|
5
5
|
import { ModuleMap } from '../../modules/modules';
|
|
6
6
|
export declare enum PackageContext {
|
|
7
7
|
Outside = "outside",
|
package/core/class/pgpm.js
CHANGED
|
@@ -49,12 +49,12 @@ const path_1 = __importStar(require("path"));
|
|
|
49
49
|
const pg_cache_1 = require("pg-cache");
|
|
50
50
|
const yanse_1 = __importDefault(require("yanse"));
|
|
51
51
|
const extensions_1 = require("../../extensions/extensions");
|
|
52
|
-
const files_1 = require("
|
|
53
|
-
const files_2 = require("
|
|
54
|
-
const writer_1 = require("
|
|
55
|
-
const generator_1 = require("
|
|
56
|
-
const parser_1 = require("
|
|
57
|
-
const validators_1 = require("
|
|
52
|
+
const files_1 = require("@pgpmjs/ast/files");
|
|
53
|
+
const files_2 = require("@pgpmjs/ast/files");
|
|
54
|
+
const writer_1 = require("@pgpmjs/ast/files/extension/writer");
|
|
55
|
+
const generator_1 = require("@pgpmjs/ast/files/plan/generator");
|
|
56
|
+
const parser_1 = require("@pgpmjs/ast/files/plan/parser");
|
|
57
|
+
const validators_1 = require("@pgpmjs/ast/files/plan/validators");
|
|
58
58
|
const client_1 = require("../../migrate/client");
|
|
59
59
|
const modules_1 = require("../../modules/modules");
|
|
60
60
|
const package_1 = require("../../packaging/package");
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { materializeBundle, verifyBundle } from '@pgpmjs/bundle';
|
|
5
|
+
import { PgpmMigrate } from '../migrate/client';
|
|
6
|
+
/**
|
|
7
|
+
* Analyze a bundle apply without executing it: verify artifact integrity, run
|
|
8
|
+
* the optional namespace-reference validator, and derive the rollback plan.
|
|
9
|
+
* Pure — no database, no disk.
|
|
10
|
+
*/
|
|
11
|
+
export function previewBundleApply(bundle, options = {}) {
|
|
12
|
+
const namespaceViolations = [];
|
|
13
|
+
if (options.validateReferences) {
|
|
14
|
+
for (const change of bundle.changes) {
|
|
15
|
+
for (const script of [change.deploy, change.revert, change.verify]) {
|
|
16
|
+
if (!script)
|
|
17
|
+
continue;
|
|
18
|
+
const references = options.validateReferences(script.sql, {
|
|
19
|
+
change: change.name,
|
|
20
|
+
kind: script.kind
|
|
21
|
+
});
|
|
22
|
+
if (references.length > 0) {
|
|
23
|
+
namespaceViolations.push({ change: change.name, kind: script.kind, references });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name: bundle.manifest.name,
|
|
30
|
+
deployOrder: [...bundle.manifest.deployOrder],
|
|
31
|
+
bundleIssues: verifyBundle(bundle),
|
|
32
|
+
namespaceViolations,
|
|
33
|
+
rollbackPlan: [...bundle.manifest.deployOrder].reverse()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Thrown when an apply is refused pre-flight or exceeds its timeout. */
|
|
37
|
+
export class BundleApplyError extends Error {
|
|
38
|
+
preview;
|
|
39
|
+
timedOut;
|
|
40
|
+
constructor(message, preview, timedOut = false) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.preview = preview;
|
|
43
|
+
this.timedOut = timedOut;
|
|
44
|
+
this.name = 'BundleApplyError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const TIMEOUT = Symbol('timeout');
|
|
48
|
+
async function withTimeout(work, ms) {
|
|
49
|
+
if (!ms)
|
|
50
|
+
return work;
|
|
51
|
+
let timer;
|
|
52
|
+
const timeout = new Promise(resolve => {
|
|
53
|
+
timer = setTimeout(() => resolve(TIMEOUT), ms);
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
return await Promise.race([work, timeout]);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Safely apply a {@link MigrationBundle} to a target database.
|
|
64
|
+
*
|
|
65
|
+
* A thin, safety-first wrapper over {@link PgpmMigrate}: it gates on artifact
|
|
66
|
+
* integrity and (optionally) namespace validation, supports a dry run, applies
|
|
67
|
+
* atomically in a single transaction, enforces a timeout, and returns an
|
|
68
|
+
* explicit result including the rollback plan.
|
|
69
|
+
*
|
|
70
|
+
* The mechanics (materialize → deploy → verify) reuse existing plumbing;
|
|
71
|
+
* nothing here re-implements deployment.
|
|
72
|
+
*/
|
|
73
|
+
export async function applyBundle(bundle, options) {
|
|
74
|
+
const started = Date.now();
|
|
75
|
+
const preview = previewBundleApply(bundle, options);
|
|
76
|
+
if (!options.allowUnverified && preview.bundleIssues.length > 0) {
|
|
77
|
+
throw new BundleApplyError(`Refusing to apply "${preview.name}": bundle failed integrity verification ` +
|
|
78
|
+
`(${preview.bundleIssues.length} issue(s): ${preview.bundleIssues.map(i => i.kind).join(', ')})`, preview);
|
|
79
|
+
}
|
|
80
|
+
if (preview.namespaceViolations.length > 0) {
|
|
81
|
+
throw new BundleApplyError(`Refusing to apply "${preview.name}": ${preview.namespaceViolations.length} ` +
|
|
82
|
+
`reference(s) fall outside the target namespace`, preview);
|
|
83
|
+
}
|
|
84
|
+
if (options.dryRun) {
|
|
85
|
+
return {
|
|
86
|
+
preview,
|
|
87
|
+
dryRun: true,
|
|
88
|
+
executed: false,
|
|
89
|
+
durationMs: Date.now() - started,
|
|
90
|
+
timedOut: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const ownWorkDir = options.workDir === undefined;
|
|
94
|
+
const workDir = options.workDir ?? mkdtempSync(join(tmpdir(), 'pgpm-apply-'));
|
|
95
|
+
materializeBundle(bundle, workDir);
|
|
96
|
+
const migrate = new PgpmMigrate(options.config, options.migrateOptions);
|
|
97
|
+
const useTransaction = options.useTransaction ?? true;
|
|
98
|
+
try {
|
|
99
|
+
const run = (async () => {
|
|
100
|
+
const deploy = await migrate.deploy({ modulePath: workDir, useTransaction });
|
|
101
|
+
let verify;
|
|
102
|
+
if (options.verify) {
|
|
103
|
+
verify = await migrate.verify({ modulePath: workDir });
|
|
104
|
+
}
|
|
105
|
+
return { deploy, verify };
|
|
106
|
+
})();
|
|
107
|
+
const outcome = await withTimeout(run, options.timeoutMs);
|
|
108
|
+
if (outcome === TIMEOUT) {
|
|
109
|
+
throw new BundleApplyError(`Apply of "${preview.name}" exceeded ${options.timeoutMs}ms`, preview, true);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
preview,
|
|
113
|
+
dryRun: false,
|
|
114
|
+
executed: true,
|
|
115
|
+
deploy: outcome.deploy,
|
|
116
|
+
verify: outcome.verify,
|
|
117
|
+
durationMs: Date.now() - started,
|
|
118
|
+
timedOut: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (ownWorkDir) {
|
|
123
|
+
try {
|
|
124
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// best-effort cleanup
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgpm migration bundles: the portable, content-addressed artifact layer.
|
|
3
|
+
*
|
|
4
|
+
* A {@link MigrationBundle} is a deterministic snapshot of a pgpm module — plan,
|
|
5
|
+
* control, ordered changes, and per-change/whole-bundle sha256 digests — with an
|
|
6
|
+
* optional provenance manifest. It is the substrate for the `exportMigrationBundle`
|
|
7
|
+
* / `applyMigrationBundle` / `transpileMigrationBundle` cloud functions: build one
|
|
8
|
+
* from a module AST, transport or transform it, verify its digests, then
|
|
9
|
+
* materialize it back into a deployable module.
|
|
10
|
+
*/
|
|
11
|
+
// Pure artifact layer moved to @pgpmjs/bundle; re-exported here so existing
|
|
12
|
+
// `@pgpmjs/core` consumers keep importing the whole bundle surface unchanged.
|
|
13
|
+
export * from '@pgpmjs/bundle';
|
|
14
|
+
// applyBundle stays in core: it drives PgpmMigrate (the deploy engine).
|
|
15
|
+
export * from './apply';
|
package/esm/core/class/pgpm.js
CHANGED
|
@@ -10,12 +10,12 @@ import path, { dirname, resolve } from 'path';
|
|
|
10
10
|
import { getPgPool } from 'pg-cache';
|
|
11
11
|
import yanse from 'yanse';
|
|
12
12
|
import { getAvailableExtensions } from '../../extensions/extensions';
|
|
13
|
-
import { generatePlan, writePlan, writePlanFile } from '
|
|
14
|
-
import { getExtensionInfo, getExtensionName, getInstalledExtensions, parseControlFile, writeExtensions } from '
|
|
15
|
-
import { generateControlFileContent, writeExtensionMakefile } from '
|
|
16
|
-
import { getNow as getPlanTimestamp } from '
|
|
17
|
-
import { parsePlanFile } from '
|
|
18
|
-
import { isValidChangeName, isValidTagName, parseReference } from '
|
|
13
|
+
import { generatePlan, writePlan, writePlanFile } from '@pgpmjs/ast/files';
|
|
14
|
+
import { getExtensionInfo, getExtensionName, getInstalledExtensions, parseControlFile, writeExtensions } from '@pgpmjs/ast/files';
|
|
15
|
+
import { generateControlFileContent, writeExtensionMakefile } from '@pgpmjs/ast/files/extension/writer';
|
|
16
|
+
import { getNow as getPlanTimestamp } from '@pgpmjs/ast/files/plan/generator';
|
|
17
|
+
import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
|
|
18
|
+
import { isValidChangeName, isValidTagName, parseReference } from '@pgpmjs/ast/files/plan/validators';
|
|
19
19
|
import { PgpmMigrate } from '../../migrate/client';
|
|
20
20
|
import { getExtensionsAndModules, getExtensionsAndModulesChanges, latestChange, latestChangeAndVersion } from '../../modules/modules';
|
|
21
21
|
import { packageModule } from '../../packaging/package';
|
package/esm/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './core/class/pgpm';
|
|
2
2
|
export * from './slice';
|
|
3
|
+
export * from './rebundle';
|
|
3
4
|
export * from './extensions/extensions';
|
|
4
5
|
export * from './modules/modules';
|
|
5
6
|
export * from './packaging/package';
|
|
@@ -8,16 +9,19 @@ export * from './resolution/deps';
|
|
|
8
9
|
export * from './resolution/resolve';
|
|
9
10
|
export * from './workspace/paths';
|
|
10
11
|
export * from './workspace/utils';
|
|
12
|
+
export * from './workspace/minimal';
|
|
11
13
|
export * from './core/template-scaffold';
|
|
12
14
|
export * from './core/boilerplate-types';
|
|
13
15
|
export * from './core/boilerplate-scanner';
|
|
14
|
-
//
|
|
15
|
-
export * from '
|
|
16
|
+
// Re-export the module AST layer (moved to the @pgpmjs/ast leaf package)
|
|
17
|
+
export * from '@pgpmjs/ast/files';
|
|
18
|
+
export * from '@pgpmjs/ast/module';
|
|
19
|
+
export * from './bundle';
|
|
16
20
|
export * from './refactor';
|
|
17
21
|
export { cleanSql } from './migrate/clean';
|
|
18
22
|
export { PgpmMigrate } from './migrate/client';
|
|
19
23
|
export { PgpmInit } from './init/client';
|
|
20
24
|
export * from './roles';
|
|
21
|
-
export { parseAuthor } from '
|
|
25
|
+
export { parseAuthor } from '@pgpmjs/ast';
|
|
22
26
|
export { hashFile, hashString } from './migrate/utils/hash';
|
|
23
27
|
export { executeQuery, withTransaction } from './migrate/utils/transaction';
|
package/esm/migrate/client.js
CHANGED
|
@@ -3,7 +3,7 @@ import { errors, extractPgErrorFields, formatPgError, formatPgErrorFields } from
|
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { getPgPool } from 'pg-cache';
|
|
6
|
-
import { parsePlanFile, parsePlanFileSimple, readScript } from '
|
|
6
|
+
import { parsePlanFile, parsePlanFileSimple, readScript } from '@pgpmjs/ast/files';
|
|
7
7
|
import { resolveDependencies } from '../resolution/deps';
|
|
8
8
|
import { resolveTagToChangeName } from '../resolution/resolve';
|
|
9
9
|
import { cleanSql } from './clean';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { hashString } from '@pgpmjs/ast';
|
|
1
2
|
import { createHash } from 'crypto';
|
|
2
3
|
import { readFile } from 'fs/promises';
|
|
3
4
|
import { parse } from 'pgsql-parser';
|
|
4
5
|
import { cleanTree } from '../../packaging/package';
|
|
6
|
+
// Re-exported from @pgpmjs/ast so existing `@pgpmjs/core` consumers keep importing it here.
|
|
7
|
+
export { hashString };
|
|
5
8
|
/**
|
|
6
9
|
* Generate SHA256 hash of a file's contents
|
|
7
10
|
*/
|
|
@@ -9,12 +12,6 @@ export async function hashFile(filePath) {
|
|
|
9
12
|
const content = await readFile(filePath, 'utf-8');
|
|
10
13
|
return createHash('sha256').update(content).digest('hex');
|
|
11
14
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Generate SHA256 hash of a string
|
|
14
|
-
*/
|
|
15
|
-
export function hashString(content) {
|
|
16
|
-
return createHash('sha256').update(content).digest('hex');
|
|
17
|
-
}
|
|
18
15
|
/**
|
|
19
16
|
* Generate SHA256 hash of a SQL file's parsed and cleaned AST
|
|
20
17
|
*/
|
package/esm/modules/modules.js
CHANGED
package/esm/packaging/package.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { relative } from 'path';
|
|
4
4
|
import { deparse } from 'pgsql-deparser';
|
|
5
5
|
import { parse } from 'pgsql-parser';
|
|
6
|
-
import { getExtensionName } from '
|
|
6
|
+
import { getExtensionName } from '@pgpmjs/ast/files';
|
|
7
7
|
import { resolve, resolveWithPlan } from '../resolution/resolve';
|
|
8
8
|
import { transformProps } from './transform';
|
|
9
9
|
const log = new Logger('package');
|
|
@@ -15,8 +15,8 @@ export const cleanTree = (tree) => {
|
|
|
15
15
|
location: noop,
|
|
16
16
|
});
|
|
17
17
|
};
|
|
18
|
-
const filterStatements = (stmts,
|
|
19
|
-
if (!
|
|
18
|
+
const filterStatements = (stmts, stripTransactions) => {
|
|
19
|
+
if (!stripTransactions)
|
|
20
20
|
return stmts;
|
|
21
21
|
return stmts.filter(node => {
|
|
22
22
|
const stmt = node.stmt;
|
|
@@ -24,7 +24,39 @@ const filterStatements = (stmts, extension) => {
|
|
|
24
24
|
!stmt.hasOwnProperty('CreateExtensionStmt');
|
|
25
25
|
});
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Merge a concatenated SQL string into a single deparsed script.
|
|
29
|
+
*
|
|
30
|
+
* Parses the input, optionally strips transaction/extension statements, deparses
|
|
31
|
+
* with a stable function delimiter, and round-trips the result to detect any AST
|
|
32
|
+
* drift. This is the shared engine behind `packageModule` (whole-module packaging)
|
|
33
|
+
* and per-chunk rebundling — both must merge statements identically.
|
|
34
|
+
*/
|
|
35
|
+
export const mergeSqlStatements = async (sql, { stripTransactions = false, echoExtensionName, pretty = true, functionDelimiter = '$EOFCODE$' } = {}) => {
|
|
36
|
+
if (!sql?.trim()) {
|
|
37
|
+
return { sql: '' };
|
|
38
|
+
}
|
|
39
|
+
const parsed = await parse(sql);
|
|
40
|
+
parsed.stmts = filterStatements(parsed.stmts, stripTransactions);
|
|
41
|
+
const topLine = echoExtensionName
|
|
42
|
+
? `\\echo Use "CREATE EXTENSION ${echoExtensionName}" to load this file. \\quit\n`
|
|
43
|
+
: '';
|
|
44
|
+
const finalSql = await deparse(parsed, { pretty, functionDelimiter });
|
|
45
|
+
const tree1 = parsed.stmts;
|
|
46
|
+
const reparsed = await parse(finalSql);
|
|
47
|
+
const tree2 = filterStatements(reparsed.stmts, stripTransactions);
|
|
48
|
+
const results = {
|
|
49
|
+
sql: `${topLine}${finalSql}`,
|
|
50
|
+
};
|
|
51
|
+
const diff = JSON.stringify(cleanTree(tree1)) !== JSON.stringify(cleanTree(tree2));
|
|
52
|
+
if (diff) {
|
|
53
|
+
results.diff = true;
|
|
54
|
+
results.tree1 = JSON.stringify(cleanTree(tree1), null, 2);
|
|
55
|
+
results.tree2 = JSON.stringify(cleanTree(tree2), null, 2);
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
};
|
|
59
|
+
export const packageModule = async (packageDir, { usePlan = true, extension = true, pretty = true, functionDelimiter = '$EOFCODE$' } = {}) => {
|
|
28
60
|
const resolveFn = usePlan ? resolveWithPlan : resolve;
|
|
29
61
|
const sql = resolveFn(packageDir);
|
|
30
62
|
if (!sql?.trim()) {
|
|
@@ -33,28 +65,12 @@ export const packageModule = async (packageDir, { usePlan = true, extension = tr
|
|
|
33
65
|
}
|
|
34
66
|
const extname = getExtensionName(packageDir);
|
|
35
67
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
? `\\echo Use "CREATE EXTENSION ${extname}" to load this file. \\quit\n`
|
|
40
|
-
: '';
|
|
41
|
-
const finalSql = await deparse(parsed, {
|
|
68
|
+
return await mergeSqlStatements(sql, {
|
|
69
|
+
stripTransactions: extension,
|
|
70
|
+
echoExtensionName: extension ? extname : undefined,
|
|
42
71
|
pretty,
|
|
43
|
-
functionDelimiter
|
|
72
|
+
functionDelimiter,
|
|
44
73
|
});
|
|
45
|
-
const tree1 = parsed.stmts;
|
|
46
|
-
const reparsed = await parse(finalSql);
|
|
47
|
-
const tree2 = filterStatements(reparsed.stmts, extension);
|
|
48
|
-
const results = {
|
|
49
|
-
sql: `${topLine}${finalSql}`,
|
|
50
|
-
};
|
|
51
|
-
const diff = JSON.stringify(cleanTree(tree1)) !== JSON.stringify(cleanTree(tree2));
|
|
52
|
-
if (diff) {
|
|
53
|
-
results.diff = true;
|
|
54
|
-
results.tree1 = JSON.stringify(cleanTree(tree1), null, 2);
|
|
55
|
-
results.tree2 = JSON.stringify(cleanTree(tree2), null, 2);
|
|
56
|
-
}
|
|
57
|
-
return results;
|
|
58
74
|
}
|
|
59
75
|
catch (e) {
|
|
60
76
|
log.error(`❌ Failed to parse SQL for ${packageDir}`);
|