@pgpmjs/core 3.0.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/LICENSE +23 -0
- package/README.md +99 -0
- package/core/boilerplate-scanner.d.ts +41 -0
- package/core/boilerplate-scanner.js +106 -0
- package/core/boilerplate-types.d.ts +52 -0
- package/core/boilerplate-types.js +6 -0
- package/core/class/pgpm.d.ts +150 -0
- package/core/class/pgpm.js +1470 -0
- package/core/template-scaffold.d.ts +29 -0
- package/core/template-scaffold.js +168 -0
- package/esm/core/boilerplate-scanner.js +96 -0
- package/esm/core/boilerplate-types.js +5 -0
- package/esm/core/class/pgpm.js +1430 -0
- package/esm/core/template-scaffold.js +161 -0
- package/esm/export/export-meta.js +240 -0
- package/esm/export/export-migrations.js +180 -0
- package/esm/extensions/extensions.js +31 -0
- package/esm/files/extension/index.js +3 -0
- package/esm/files/extension/reader.js +79 -0
- package/esm/files/extension/writer.js +63 -0
- package/esm/files/index.js +6 -0
- package/esm/files/plan/generator.js +49 -0
- package/esm/files/plan/index.js +5 -0
- package/esm/files/plan/parser.js +296 -0
- package/esm/files/plan/validators.js +181 -0
- package/esm/files/plan/writer.js +114 -0
- package/esm/files/sql/index.js +1 -0
- package/esm/files/sql/writer.js +107 -0
- package/esm/files/sql-scripts/index.js +2 -0
- package/esm/files/sql-scripts/reader.js +19 -0
- package/esm/files/types/index.js +1 -0
- package/esm/files/types/package.js +1 -0
- package/esm/index.js +21 -0
- package/esm/init/client.js +144 -0
- package/esm/init/sql/bootstrap-roles.sql +55 -0
- package/esm/init/sql/bootstrap-test-roles.sql +72 -0
- package/esm/migrate/clean.js +23 -0
- package/esm/migrate/client.js +551 -0
- package/esm/migrate/index.js +5 -0
- package/esm/migrate/sql/procedures.sql +258 -0
- package/esm/migrate/sql/schema.sql +37 -0
- package/esm/migrate/types.js +1 -0
- package/esm/migrate/utils/event-logger.js +28 -0
- package/esm/migrate/utils/hash.js +27 -0
- package/esm/migrate/utils/transaction.js +125 -0
- package/esm/modules/modules.js +49 -0
- package/esm/packaging/package.js +96 -0
- package/esm/packaging/transform.js +70 -0
- package/esm/projects/deploy.js +123 -0
- package/esm/projects/revert.js +75 -0
- package/esm/projects/verify.js +61 -0
- package/esm/resolution/deps.js +526 -0
- package/esm/resolution/resolve.js +101 -0
- package/esm/utils/debug.js +147 -0
- package/esm/utils/target-utils.js +37 -0
- package/esm/workspace/paths.js +43 -0
- package/esm/workspace/utils.js +31 -0
- package/export/export-meta.d.ts +8 -0
- package/export/export-meta.js +244 -0
- package/export/export-migrations.d.ts +17 -0
- package/export/export-migrations.js +187 -0
- package/extensions/extensions.d.ts +5 -0
- package/extensions/extensions.js +35 -0
- package/files/extension/index.d.ts +2 -0
- package/files/extension/index.js +19 -0
- package/files/extension/reader.d.ts +24 -0
- package/files/extension/reader.js +86 -0
- package/files/extension/writer.d.ts +39 -0
- package/files/extension/writer.js +70 -0
- package/files/index.d.ts +5 -0
- package/files/index.js +22 -0
- package/files/plan/generator.d.ts +22 -0
- package/files/plan/generator.js +57 -0
- package/files/plan/index.d.ts +4 -0
- package/files/plan/index.js +21 -0
- package/files/plan/parser.d.ts +27 -0
- package/files/plan/parser.js +303 -0
- package/files/plan/validators.d.ts +52 -0
- package/files/plan/validators.js +187 -0
- package/files/plan/writer.d.ts +27 -0
- package/files/plan/writer.js +124 -0
- package/files/sql/index.d.ts +1 -0
- package/files/sql/index.js +17 -0
- package/files/sql/writer.d.ts +12 -0
- package/files/sql/writer.js +114 -0
- package/files/sql-scripts/index.d.ts +1 -0
- package/files/sql-scripts/index.js +18 -0
- package/files/sql-scripts/reader.d.ts +8 -0
- package/files/sql-scripts/reader.js +23 -0
- package/files/types/index.d.ts +46 -0
- package/files/types/index.js +17 -0
- package/files/types/package.d.ts +20 -0
- package/files/types/package.js +2 -0
- package/index.d.ts +21 -0
- package/index.js +45 -0
- package/init/client.d.ts +26 -0
- package/init/client.js +148 -0
- package/init/sql/bootstrap-roles.sql +55 -0
- package/init/sql/bootstrap-test-roles.sql +72 -0
- package/migrate/clean.d.ts +1 -0
- package/migrate/clean.js +27 -0
- package/migrate/client.d.ts +80 -0
- package/migrate/client.js +555 -0
- package/migrate/index.d.ts +5 -0
- package/migrate/index.js +21 -0
- package/migrate/sql/procedures.sql +258 -0
- package/migrate/sql/schema.sql +37 -0
- package/migrate/types.d.ts +67 -0
- package/migrate/types.js +2 -0
- package/migrate/utils/event-logger.d.ts +13 -0
- package/migrate/utils/event-logger.js +32 -0
- package/migrate/utils/hash.d.ts +12 -0
- package/migrate/utils/hash.js +32 -0
- package/migrate/utils/transaction.d.ts +27 -0
- package/migrate/utils/transaction.js +129 -0
- package/modules/modules.d.ts +31 -0
- package/modules/modules.js +56 -0
- package/package.json +70 -0
- package/packaging/package.d.ts +19 -0
- package/packaging/package.js +102 -0
- package/packaging/transform.d.ts +22 -0
- package/packaging/transform.js +75 -0
- package/projects/deploy.d.ts +8 -0
- package/projects/deploy.js +160 -0
- package/projects/revert.d.ts +15 -0
- package/projects/revert.js +112 -0
- package/projects/verify.d.ts +8 -0
- package/projects/verify.js +98 -0
- package/resolution/deps.d.ts +57 -0
- package/resolution/deps.js +531 -0
- package/resolution/resolve.d.ts +37 -0
- package/resolution/resolve.js +107 -0
- package/utils/debug.d.ts +21 -0
- package/utils/debug.js +153 -0
- package/utils/target-utils.d.ts +5 -0
- package/utils/target-utils.js +40 -0
- package/workspace/paths.d.ts +14 -0
- package/workspace/paths.js +50 -0
- package/workspace/utils.d.ts +8 -0
- package/workspace/utils.js +36 -0
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PgpmPackage = exports.PackageContext = void 0;
|
|
40
|
+
const env_1 = require("@pgpmjs/env");
|
|
41
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
42
|
+
const types_1 = require("@pgpmjs/types");
|
|
43
|
+
const yanse_1 = __importDefault(require("yanse"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const fs_1 = __importDefault(require("fs"));
|
|
46
|
+
const glob = __importStar(require("glob"));
|
|
47
|
+
const os_1 = __importDefault(require("os"));
|
|
48
|
+
const parse_package_name_1 = require("parse-package-name");
|
|
49
|
+
const path_1 = __importStar(require("path"));
|
|
50
|
+
const pg_cache_1 = require("pg-cache");
|
|
51
|
+
const template_scaffold_1 = require("../template-scaffold");
|
|
52
|
+
const extensions_1 = require("../../extensions/extensions");
|
|
53
|
+
const files_1 = require("../../files");
|
|
54
|
+
const parser_1 = require("../../files/plan/parser");
|
|
55
|
+
const validators_1 = require("../../files/plan/validators");
|
|
56
|
+
const generator_1 = require("../../files/plan/generator");
|
|
57
|
+
const files_2 = require("../../files");
|
|
58
|
+
const writer_1 = require("../../files/extension/writer");
|
|
59
|
+
const client_1 = require("../../migrate/client");
|
|
60
|
+
const modules_1 = require("../../modules/modules");
|
|
61
|
+
const package_1 = require("../../packaging/package");
|
|
62
|
+
const deps_1 = require("../../resolution/deps");
|
|
63
|
+
const target_utils_1 = require("../../utils/target-utils");
|
|
64
|
+
const logger = new logger_1.Logger('pgpm');
|
|
65
|
+
function getUTCTimestamp(d = new Date()) {
|
|
66
|
+
return (d.getUTCFullYear() +
|
|
67
|
+
'-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
|
|
68
|
+
'-' + String(d.getUTCDate()).padStart(2, '0') +
|
|
69
|
+
'T' + String(d.getUTCHours()).padStart(2, '0') +
|
|
70
|
+
':' + String(d.getUTCMinutes()).padStart(2, '0') +
|
|
71
|
+
':' + String(d.getUTCSeconds()).padStart(2, '0') +
|
|
72
|
+
'Z');
|
|
73
|
+
}
|
|
74
|
+
function sortObjectByKey(obj) {
|
|
75
|
+
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
76
|
+
}
|
|
77
|
+
const getNow = () => process.env.NODE_ENV === 'test'
|
|
78
|
+
? getUTCTimestamp(new Date('2017-08-11T08:11:51Z'))
|
|
79
|
+
: getUTCTimestamp(new Date());
|
|
80
|
+
/**
|
|
81
|
+
* Truncates workspace extensions to include only modules from the target onwards.
|
|
82
|
+
* This prevents processing unnecessary modules that come before the target in dependency order.
|
|
83
|
+
*
|
|
84
|
+
* @param workspaceExtensions - The full workspace extension dependencies
|
|
85
|
+
* @param targetName - The target module name to truncate from
|
|
86
|
+
* @returns Truncated extensions starting from the target module
|
|
87
|
+
*/
|
|
88
|
+
const truncateExtensionsToTarget = (workspaceExtensions, targetName) => {
|
|
89
|
+
const targetIndex = workspaceExtensions.resolved.indexOf(targetName);
|
|
90
|
+
if (targetIndex === -1) {
|
|
91
|
+
return workspaceExtensions;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
resolved: workspaceExtensions.resolved.slice(targetIndex),
|
|
95
|
+
external: workspaceExtensions.external
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
var PackageContext;
|
|
99
|
+
(function (PackageContext) {
|
|
100
|
+
PackageContext["Outside"] = "outside";
|
|
101
|
+
PackageContext["Workspace"] = "workspace-root";
|
|
102
|
+
PackageContext["Module"] = "module";
|
|
103
|
+
PackageContext["ModuleInsideWorkspace"] = "module-in-workspace";
|
|
104
|
+
})(PackageContext || (exports.PackageContext = PackageContext = {}));
|
|
105
|
+
class PgpmPackage {
|
|
106
|
+
cwd;
|
|
107
|
+
workspacePath;
|
|
108
|
+
modulePath;
|
|
109
|
+
config;
|
|
110
|
+
allowedDirs = [];
|
|
111
|
+
allowedParentDirs = [];
|
|
112
|
+
_moduleMap;
|
|
113
|
+
_moduleInfo;
|
|
114
|
+
constructor(cwd = process.cwd()) {
|
|
115
|
+
this.resetCwd(cwd);
|
|
116
|
+
}
|
|
117
|
+
resetCwd(cwd) {
|
|
118
|
+
this.cwd = cwd;
|
|
119
|
+
this.workspacePath = (0, env_1.resolvePgpmPath)(this.cwd);
|
|
120
|
+
this.modulePath = this.resolveSqitchPath();
|
|
121
|
+
if (this.workspacePath) {
|
|
122
|
+
this.config = this.loadConfigSync();
|
|
123
|
+
this.allowedDirs = this.loadAllowedDirs();
|
|
124
|
+
this.allowedParentDirs = this.loadAllowedParentDirs();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
resolveSqitchPath() {
|
|
128
|
+
try {
|
|
129
|
+
return (0, env_1.walkUp)(this.cwd, 'pgpm.plan');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
loadConfigSync() {
|
|
136
|
+
return (0, env_1.loadConfigSyncFromDir)(this.workspacePath);
|
|
137
|
+
}
|
|
138
|
+
loadAllowedDirs() {
|
|
139
|
+
const globs = this.config?.packages ?? [];
|
|
140
|
+
const dirs = globs.flatMap(pattern => glob.sync(path_1.default.join(this.workspacePath, pattern)));
|
|
141
|
+
const resolvedDirs = dirs.map(dir => path_1.default.resolve(dir));
|
|
142
|
+
// Remove duplicates by converting to Set and back to array
|
|
143
|
+
return [...new Set(resolvedDirs)];
|
|
144
|
+
}
|
|
145
|
+
loadAllowedParentDirs() {
|
|
146
|
+
const globs = this.config?.packages ?? [];
|
|
147
|
+
const parentDirs = globs.map(pattern => {
|
|
148
|
+
// Remove glob characters (*, **, ?, etc.) to get the base path
|
|
149
|
+
const basePath = pattern.replace(/[*?[\]{}]/g, '').replace(/\/$/, '');
|
|
150
|
+
return path_1.default.resolve(this.workspacePath, basePath);
|
|
151
|
+
});
|
|
152
|
+
// Remove duplicates by converting to Set and back to array
|
|
153
|
+
return [...new Set(parentDirs)];
|
|
154
|
+
}
|
|
155
|
+
isInsideAllowedDirs(cwd) {
|
|
156
|
+
return this.allowedDirs.some(dir => cwd.startsWith(dir));
|
|
157
|
+
}
|
|
158
|
+
isParentOfAllowedDirs(cwd) {
|
|
159
|
+
const resolvedCwd = path_1.default.resolve(cwd);
|
|
160
|
+
return this.allowedDirs.some(dir => dir.startsWith(resolvedCwd + path_1.default.sep)) ||
|
|
161
|
+
this.allowedParentDirs.some(dir => path_1.default.resolve(dir) === resolvedCwd);
|
|
162
|
+
}
|
|
163
|
+
createModuleDirectory(modName) {
|
|
164
|
+
this.ensureWorkspace();
|
|
165
|
+
const isRoot = path_1.default.resolve(this.workspacePath) === path_1.default.resolve(this.cwd);
|
|
166
|
+
const isParentDir = this.isParentOfAllowedDirs(this.cwd);
|
|
167
|
+
const isInsideModule = this.isInsideAllowedDirs(this.cwd);
|
|
168
|
+
let targetPath;
|
|
169
|
+
if (isRoot) {
|
|
170
|
+
const packagesDir = path_1.default.join(this.cwd, 'packages');
|
|
171
|
+
fs_1.default.mkdirSync(packagesDir, { recursive: true });
|
|
172
|
+
targetPath = path_1.default.join(packagesDir, modName);
|
|
173
|
+
}
|
|
174
|
+
else if (isParentDir) {
|
|
175
|
+
targetPath = path_1.default.join(this.cwd, modName);
|
|
176
|
+
}
|
|
177
|
+
else if (isInsideModule) {
|
|
178
|
+
console.error(yanse_1.default.red(`Error: Cannot create a module inside an existing module. Please run 'lql init' from the workspace root or from a parent directory like 'packages/'.`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.error(yanse_1.default.red(`Error: You must be inside the workspace root, a parent directory of modules (like 'packages/'), or inside one of the workspace packages: ${this.allowedDirs.join(', ')}`));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
fs_1.default.mkdirSync(targetPath, { recursive: true });
|
|
186
|
+
return targetPath;
|
|
187
|
+
}
|
|
188
|
+
ensureModule() {
|
|
189
|
+
if (!this.modulePath)
|
|
190
|
+
throw new Error('Not inside a module');
|
|
191
|
+
}
|
|
192
|
+
ensureWorkspace() {
|
|
193
|
+
if (!this.workspacePath)
|
|
194
|
+
throw new Error('Not inside a workspace');
|
|
195
|
+
}
|
|
196
|
+
getContext() {
|
|
197
|
+
if (this.modulePath && this.workspacePath) {
|
|
198
|
+
const rel = path_1.default.relative(this.workspacePath, this.modulePath);
|
|
199
|
+
const nested = !rel.startsWith('..') && !path_1.default.isAbsolute(rel);
|
|
200
|
+
return nested ? PackageContext.ModuleInsideWorkspace : PackageContext.Module;
|
|
201
|
+
}
|
|
202
|
+
if (this.modulePath)
|
|
203
|
+
return PackageContext.Module;
|
|
204
|
+
if (this.workspacePath)
|
|
205
|
+
return PackageContext.Workspace;
|
|
206
|
+
return PackageContext.Outside;
|
|
207
|
+
}
|
|
208
|
+
isInWorkspace() {
|
|
209
|
+
return this.getContext() === PackageContext.Workspace;
|
|
210
|
+
}
|
|
211
|
+
isInModule() {
|
|
212
|
+
return (this.getContext() === PackageContext.Module ||
|
|
213
|
+
this.getContext() === PackageContext.ModuleInsideWorkspace);
|
|
214
|
+
}
|
|
215
|
+
getWorkspacePath() {
|
|
216
|
+
return this.workspacePath;
|
|
217
|
+
}
|
|
218
|
+
getModulePath() {
|
|
219
|
+
return this.modulePath;
|
|
220
|
+
}
|
|
221
|
+
clearCache() {
|
|
222
|
+
delete this._moduleInfo;
|
|
223
|
+
delete this._moduleMap;
|
|
224
|
+
}
|
|
225
|
+
// ──────────────── Workspace-wide ────────────────
|
|
226
|
+
async getModules() {
|
|
227
|
+
if (!this.workspacePath || !this.config)
|
|
228
|
+
return [];
|
|
229
|
+
const dirs = this.loadAllowedDirs();
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const dir of dirs) {
|
|
232
|
+
const proj = new PgpmPackage(dir);
|
|
233
|
+
if (proj.isInModule()) {
|
|
234
|
+
results.push(proj);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return results;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* List all modules by parsing .control files in the workspace directory.
|
|
241
|
+
* Handles naming collisions by preferring the shortest path.
|
|
242
|
+
*/
|
|
243
|
+
listModules() {
|
|
244
|
+
if (!this.workspacePath)
|
|
245
|
+
return {};
|
|
246
|
+
const moduleFiles = glob.sync(`${this.workspacePath}/**/*.control`).filter((file) => !/node_modules/.test(file));
|
|
247
|
+
// Group files by module name to handle collisions
|
|
248
|
+
const filesByName = new Map();
|
|
249
|
+
moduleFiles.forEach((file) => {
|
|
250
|
+
const moduleName = path_1.default.basename(file).split('.control')[0];
|
|
251
|
+
if (!filesByName.has(moduleName)) {
|
|
252
|
+
filesByName.set(moduleName, []);
|
|
253
|
+
}
|
|
254
|
+
filesByName.get(moduleName).push(file);
|
|
255
|
+
});
|
|
256
|
+
// For each module name, pick the shortest path in case of collisions
|
|
257
|
+
const selectedFiles = new Map();
|
|
258
|
+
filesByName.forEach((files, moduleName) => {
|
|
259
|
+
if (files.length === 1) {
|
|
260
|
+
selectedFiles.set(moduleName, files[0]);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Multiple files with same name - pick shortest path
|
|
264
|
+
const shortestFile = files.reduce((shortest, current) => current.length < shortest.length ? current : shortest);
|
|
265
|
+
selectedFiles.set(moduleName, shortestFile);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// Parse the selected control files
|
|
269
|
+
return Array.from(selectedFiles.entries()).reduce((acc, [moduleName, file]) => {
|
|
270
|
+
const module = (0, files_2.parseControlFile)(file, this.workspacePath);
|
|
271
|
+
acc[moduleName] = module;
|
|
272
|
+
return acc;
|
|
273
|
+
}, {});
|
|
274
|
+
}
|
|
275
|
+
getModuleMap() {
|
|
276
|
+
if (!this.workspacePath)
|
|
277
|
+
return {};
|
|
278
|
+
if (this._moduleMap)
|
|
279
|
+
return this._moduleMap;
|
|
280
|
+
this._moduleMap = this.listModules();
|
|
281
|
+
return this._moduleMap;
|
|
282
|
+
}
|
|
283
|
+
getAvailableModules() {
|
|
284
|
+
const modules = this.getModuleMap();
|
|
285
|
+
return (0, extensions_1.getAvailableExtensions)(modules);
|
|
286
|
+
}
|
|
287
|
+
getModuleProject(name) {
|
|
288
|
+
this.ensureWorkspace();
|
|
289
|
+
if (this.isInModule() && name === this.getModuleName()) {
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
const modules = this.getModuleMap();
|
|
293
|
+
if (!modules[name]) {
|
|
294
|
+
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
295
|
+
}
|
|
296
|
+
const modulePath = path_1.default.resolve(this.workspacePath, modules[name].path);
|
|
297
|
+
return new PgpmPackage(modulePath);
|
|
298
|
+
}
|
|
299
|
+
// ──────────────── Module-scoped ────────────────
|
|
300
|
+
getModuleInfo() {
|
|
301
|
+
this.ensureModule();
|
|
302
|
+
if (!this._moduleInfo) {
|
|
303
|
+
this._moduleInfo = (0, files_2.getExtensionInfo)(this.cwd);
|
|
304
|
+
}
|
|
305
|
+
return this._moduleInfo;
|
|
306
|
+
}
|
|
307
|
+
getModuleName() {
|
|
308
|
+
this.ensureModule();
|
|
309
|
+
return (0, files_2.getExtensionName)(this.cwd);
|
|
310
|
+
}
|
|
311
|
+
getRequiredModules() {
|
|
312
|
+
this.ensureModule();
|
|
313
|
+
const info = this.getModuleInfo();
|
|
314
|
+
return (0, files_2.getInstalledExtensions)(info.controlFile);
|
|
315
|
+
}
|
|
316
|
+
setModuleDependencies(modules) {
|
|
317
|
+
this.ensureModule();
|
|
318
|
+
// Validate for circular dependencies
|
|
319
|
+
this.validateModuleDependencies(modules);
|
|
320
|
+
(0, files_2.writeExtensions)(this.cwd, modules);
|
|
321
|
+
}
|
|
322
|
+
validateModuleDependencies(modules) {
|
|
323
|
+
const currentModuleName = this.getModuleName();
|
|
324
|
+
if (modules.includes(currentModuleName)) {
|
|
325
|
+
throw types_1.errors.CIRCULAR_DEPENDENCY({ module: currentModuleName, dependency: currentModuleName });
|
|
326
|
+
}
|
|
327
|
+
// Check for circular dependencies by examining each module's dependencies
|
|
328
|
+
const visited = new Set();
|
|
329
|
+
const visiting = new Set();
|
|
330
|
+
const checkCircular = (moduleName, path = []) => {
|
|
331
|
+
if (visiting.has(moduleName)) {
|
|
332
|
+
throw types_1.errors.CIRCULAR_DEPENDENCY({ module: path.join(' -> '), dependency: moduleName });
|
|
333
|
+
}
|
|
334
|
+
if (visited.has(moduleName)) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
visiting.add(moduleName);
|
|
338
|
+
// More complex dependency resolution would require loading other modules' dependencies
|
|
339
|
+
visiting.delete(moduleName);
|
|
340
|
+
visited.add(moduleName);
|
|
341
|
+
};
|
|
342
|
+
modules.forEach(module => checkCircular(module, [currentModuleName]));
|
|
343
|
+
}
|
|
344
|
+
initModuleSqitch(modName, targetPath) {
|
|
345
|
+
const plan = (0, files_1.generatePlan)({
|
|
346
|
+
moduleName: modName,
|
|
347
|
+
uri: modName,
|
|
348
|
+
entries: []
|
|
349
|
+
});
|
|
350
|
+
(0, files_1.writePlan)(path_1.default.join(targetPath, 'pgpm.plan'), plan);
|
|
351
|
+
// Create deploy, revert, and verify directories
|
|
352
|
+
const dirs = ['deploy', 'revert', 'verify'];
|
|
353
|
+
dirs.forEach(dir => {
|
|
354
|
+
const dirPath = path_1.default.join(targetPath, dir);
|
|
355
|
+
if (!fs_1.default.existsSync(dirPath)) {
|
|
356
|
+
fs_1.default.mkdirSync(dirPath, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
async initModule(options) {
|
|
361
|
+
this.ensureWorkspace();
|
|
362
|
+
const targetPath = this.createModuleDirectory(options.name);
|
|
363
|
+
const answers = {
|
|
364
|
+
...options.answers,
|
|
365
|
+
name: options.name,
|
|
366
|
+
moduleDesc: options.description,
|
|
367
|
+
description: options.description,
|
|
368
|
+
author: options.author,
|
|
369
|
+
extensions: options.extensions
|
|
370
|
+
};
|
|
371
|
+
await (0, template_scaffold_1.scaffoldTemplate)({
|
|
372
|
+
type: 'module',
|
|
373
|
+
outputDir: targetPath,
|
|
374
|
+
templateRepo: options.templateRepo ?? template_scaffold_1.DEFAULT_TEMPLATE_REPO,
|
|
375
|
+
branch: options.branch,
|
|
376
|
+
// Don't set default templatePath - let scaffoldTemplate use metadata-driven resolution
|
|
377
|
+
templatePath: options.templatePath,
|
|
378
|
+
answers,
|
|
379
|
+
noTty: options.noTty ?? false,
|
|
380
|
+
cacheTtlMs: options.cacheTtlMs ?? template_scaffold_1.DEFAULT_TEMPLATE_TTL_MS,
|
|
381
|
+
toolName: options.toolName ?? template_scaffold_1.DEFAULT_TEMPLATE_TOOL_NAME,
|
|
382
|
+
cwd: this.cwd
|
|
383
|
+
});
|
|
384
|
+
this.initModuleSqitch(options.name, targetPath);
|
|
385
|
+
(0, files_2.writeExtensions)(targetPath, options.extensions);
|
|
386
|
+
}
|
|
387
|
+
// ──────────────── Dependency Analysis ────────────────
|
|
388
|
+
getLatestChange(moduleName) {
|
|
389
|
+
const modules = this.getModuleMap();
|
|
390
|
+
return (0, modules_1.latestChange)(moduleName, modules, this.workspacePath);
|
|
391
|
+
}
|
|
392
|
+
getLatestChangeAndVersion(moduleName) {
|
|
393
|
+
const modules = this.getModuleMap();
|
|
394
|
+
return (0, modules_1.latestChangeAndVersion)(moduleName, modules, this.workspacePath);
|
|
395
|
+
}
|
|
396
|
+
getModuleExtensions() {
|
|
397
|
+
this.ensureModule();
|
|
398
|
+
const moduleName = this.getModuleName();
|
|
399
|
+
const moduleMap = this.getModuleMap();
|
|
400
|
+
return (0, deps_1.resolveExtensionDependencies)(moduleName, moduleMap);
|
|
401
|
+
}
|
|
402
|
+
getModuleDependencies(moduleName) {
|
|
403
|
+
const modules = this.getModuleMap();
|
|
404
|
+
const { native, sqitch } = (0, modules_1.getExtensionsAndModules)(moduleName, modules);
|
|
405
|
+
return { native, modules: sqitch };
|
|
406
|
+
}
|
|
407
|
+
getModuleDependencyChanges(moduleName) {
|
|
408
|
+
const modules = this.getModuleMap();
|
|
409
|
+
const { native, sqitch } = (0, modules_1.getExtensionsAndModulesChanges)(moduleName, modules, this.workspacePath);
|
|
410
|
+
return { native, modules: sqitch };
|
|
411
|
+
}
|
|
412
|
+
// ──────────────── Plans ────────────────
|
|
413
|
+
getModulePlan() {
|
|
414
|
+
this.ensureModule();
|
|
415
|
+
const planPath = path_1.default.join(this.getModulePath(), 'pgpm.plan');
|
|
416
|
+
return fs_1.default.readFileSync(planPath, 'utf8');
|
|
417
|
+
}
|
|
418
|
+
getModuleControlFile() {
|
|
419
|
+
this.ensureModule();
|
|
420
|
+
const info = this.getModuleInfo();
|
|
421
|
+
return fs_1.default.readFileSync(info.controlFile, 'utf8');
|
|
422
|
+
}
|
|
423
|
+
getModuleMakefile() {
|
|
424
|
+
this.ensureModule();
|
|
425
|
+
const info = this.getModuleInfo();
|
|
426
|
+
return fs_1.default.readFileSync(info.Makefile, 'utf8');
|
|
427
|
+
}
|
|
428
|
+
getModuleSQL() {
|
|
429
|
+
this.ensureModule();
|
|
430
|
+
const info = this.getModuleInfo();
|
|
431
|
+
return fs_1.default.readFileSync(info.sqlFile, 'utf8');
|
|
432
|
+
}
|
|
433
|
+
generateModulePlan(options) {
|
|
434
|
+
this.ensureModule();
|
|
435
|
+
const info = this.getModuleInfo();
|
|
436
|
+
const moduleName = info.extname;
|
|
437
|
+
// Get raw dependencies and resolved list
|
|
438
|
+
const tagResolution = options.includeTags === true ? 'preserve' : 'internal';
|
|
439
|
+
let { resolved, deps } = (0, deps_1.resolveDependencies)(this.cwd, moduleName, { tagResolution });
|
|
440
|
+
// Helper to extract module name from a change reference
|
|
441
|
+
const getModuleName = (change) => {
|
|
442
|
+
const colonIndex = change.indexOf(':');
|
|
443
|
+
return colonIndex > 0 ? change.substring(0, colonIndex) : null;
|
|
444
|
+
};
|
|
445
|
+
// Helper to determine if a change is truly from an external package
|
|
446
|
+
const isExternalChange = (change) => {
|
|
447
|
+
const changeModule = getModuleName(change);
|
|
448
|
+
return changeModule !== null && changeModule !== moduleName;
|
|
449
|
+
};
|
|
450
|
+
// Helper to normalize change name (remove package prefix)
|
|
451
|
+
const normalizeChangeName = (change) => {
|
|
452
|
+
return change.includes(':') ? change.split(':').pop() : change;
|
|
453
|
+
};
|
|
454
|
+
// Clean up the resolved list to handle both formats
|
|
455
|
+
const uniqueChangeNames = new Set();
|
|
456
|
+
const normalizedResolved = [];
|
|
457
|
+
// First, add local changes without prefixes
|
|
458
|
+
resolved.forEach(change => {
|
|
459
|
+
const normalized = normalizeChangeName(change);
|
|
460
|
+
// Skip if we've already added this change
|
|
461
|
+
if (uniqueChangeNames.has(normalized))
|
|
462
|
+
return;
|
|
463
|
+
// Skip truly external changes - they should only be in dependencies
|
|
464
|
+
if (isExternalChange(change))
|
|
465
|
+
return;
|
|
466
|
+
uniqueChangeNames.add(normalized);
|
|
467
|
+
normalizedResolved.push(normalized);
|
|
468
|
+
});
|
|
469
|
+
// Clean up the deps object
|
|
470
|
+
const normalizedDeps = {};
|
|
471
|
+
// Process each deps entry
|
|
472
|
+
Object.keys(deps).forEach(key => {
|
|
473
|
+
// Normalize the key - strip "/deploy/" and ".sql" if present
|
|
474
|
+
let normalizedKey = key;
|
|
475
|
+
if (normalizedKey.startsWith('/deploy/')) {
|
|
476
|
+
normalizedKey = normalizedKey.substring(8); // Remove "/deploy/"
|
|
477
|
+
}
|
|
478
|
+
if (normalizedKey.endsWith('.sql')) {
|
|
479
|
+
normalizedKey = normalizedKey.substring(0, normalizedKey.length - 4); // Remove ".sql"
|
|
480
|
+
}
|
|
481
|
+
// Skip keys for truly external changes - we only want local changes as keys
|
|
482
|
+
if (isExternalChange(normalizedKey))
|
|
483
|
+
return;
|
|
484
|
+
// Normalize the key for all changes, removing any same-package prefix
|
|
485
|
+
const cleanKey = normalizeChangeName(normalizedKey);
|
|
486
|
+
// Build the standard key format for our normalized deps
|
|
487
|
+
const standardKey = `/deploy/${cleanKey}.sql`;
|
|
488
|
+
// Initialize the dependencies array for this key if it doesn't exist
|
|
489
|
+
normalizedDeps[standardKey] = normalizedDeps[standardKey] || [];
|
|
490
|
+
// Add dependencies, handling both formats
|
|
491
|
+
const dependencies = deps[key] || [];
|
|
492
|
+
dependencies.forEach(dep => {
|
|
493
|
+
// For truly external dependencies, keep the full reference
|
|
494
|
+
if (isExternalChange(dep)) {
|
|
495
|
+
if (!normalizedDeps[standardKey].includes(dep)) {
|
|
496
|
+
normalizedDeps[standardKey].push(dep);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// For same-package dependencies, normalize by removing prefix
|
|
501
|
+
const normalizedDep = normalizeChangeName(dep);
|
|
502
|
+
if (!normalizedDeps[standardKey].includes(normalizedDep)) {
|
|
503
|
+
normalizedDeps[standardKey].push(normalizedDep);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
// Update with normalized versions
|
|
509
|
+
resolved = normalizedResolved;
|
|
510
|
+
deps = normalizedDeps;
|
|
511
|
+
// Process external dependencies if needed
|
|
512
|
+
const includePackages = options.includePackages === true;
|
|
513
|
+
const preferTags = options.includeTags === true;
|
|
514
|
+
if (includePackages && this.workspacePath) {
|
|
515
|
+
const depData = this.getModuleDependencyChanges(moduleName);
|
|
516
|
+
if (resolved.length > 0) {
|
|
517
|
+
const firstKey = `/deploy/${resolved[0]}.sql`;
|
|
518
|
+
deps[firstKey] = deps[firstKey] || [];
|
|
519
|
+
depData.modules.forEach(m => {
|
|
520
|
+
const extModuleName = m.name;
|
|
521
|
+
const hasTagDependency = deps[firstKey].some(dep => dep.startsWith(`${extModuleName}:@`));
|
|
522
|
+
let depToken = `${extModuleName}:${m.latest}`;
|
|
523
|
+
if (preferTags) {
|
|
524
|
+
try {
|
|
525
|
+
const moduleMap = this.getModuleMap();
|
|
526
|
+
const modInfo = moduleMap[extModuleName];
|
|
527
|
+
if (modInfo && this.workspacePath) {
|
|
528
|
+
const planPath = path_1.default.join(this.workspacePath, modInfo.path, 'pgpm.plan');
|
|
529
|
+
const parsed = (0, parser_1.parsePlanFile)(planPath);
|
|
530
|
+
const changes = parsed.data?.changes || [];
|
|
531
|
+
const tags = parsed.data?.tags || [];
|
|
532
|
+
if (changes.length > 0 && tags.length > 0) {
|
|
533
|
+
const lastChangeName = changes[changes.length - 1]?.name;
|
|
534
|
+
const lastTag = tags[tags.length - 1];
|
|
535
|
+
if (lastTag && lastTag.change === lastChangeName) {
|
|
536
|
+
depToken = `${extModuleName}:@${lastTag.name}`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch { }
|
|
542
|
+
}
|
|
543
|
+
if (!hasTagDependency && !deps[firstKey].includes(depToken)) {
|
|
544
|
+
deps[firstKey].push(depToken);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// For debugging - log the cleaned structures
|
|
550
|
+
// console.log("CLEAN DEPS GRAPH", JSON.stringify(deps, null, 2));
|
|
551
|
+
// console.log("CLEAN RES GRAPH", JSON.stringify(resolved, null, 2));
|
|
552
|
+
// Prepare entries for the plan file
|
|
553
|
+
const entries = resolved.map(res => {
|
|
554
|
+
const key = `/deploy/${res}.sql`;
|
|
555
|
+
const dependencies = deps[key] || [];
|
|
556
|
+
// Filter out dependencies that match the current change name
|
|
557
|
+
// This prevents listing a change as dependent on itself
|
|
558
|
+
const filteredDeps = dependencies.filter(dep => normalizeChangeName(dep) !== res);
|
|
559
|
+
return {
|
|
560
|
+
change: res,
|
|
561
|
+
dependencies: filteredDeps,
|
|
562
|
+
comment: `add ${res}`
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
// Use the package-files package to generate the plan
|
|
566
|
+
return (0, files_1.generatePlan)({
|
|
567
|
+
moduleName,
|
|
568
|
+
uri: options.uri,
|
|
569
|
+
entries
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
writeModulePlan(options) {
|
|
573
|
+
this.ensureModule();
|
|
574
|
+
const name = this.getModuleName();
|
|
575
|
+
const plan = this.generateModulePlan(options);
|
|
576
|
+
const moduleMap = this.getModuleMap();
|
|
577
|
+
const mod = moduleMap[name];
|
|
578
|
+
const planPath = path_1.default.join(this.workspacePath, mod.path, 'pgpm.plan');
|
|
579
|
+
// Use the package-files package to write the plan
|
|
580
|
+
(0, files_1.writePlan)(planPath, plan);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Add a tag to the current module's plan file
|
|
584
|
+
*/
|
|
585
|
+
addTag(tagName, changeName, comment) {
|
|
586
|
+
this.ensureModule();
|
|
587
|
+
if (!this.modulePath) {
|
|
588
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: 'module path', type: 'module' });
|
|
589
|
+
}
|
|
590
|
+
// Validate tag name
|
|
591
|
+
if (!(0, validators_1.isValidTagName)(tagName)) {
|
|
592
|
+
throw types_1.errors.INVALID_NAME({ name: tagName, type: 'tag', rules: "Tag names must follow Sqitch naming rules and cannot contain '/'" });
|
|
593
|
+
}
|
|
594
|
+
const planPath = path_1.default.join(this.modulePath, 'pgpm.plan');
|
|
595
|
+
// Parse existing plan file
|
|
596
|
+
const planResult = (0, parser_1.parsePlanFile)(planPath);
|
|
597
|
+
if (!planResult.data) {
|
|
598
|
+
throw types_1.errors.PLAN_PARSE_ERROR({ planPath, errors: planResult.errors.map(e => e.message).join(', ') });
|
|
599
|
+
}
|
|
600
|
+
const plan = planResult.data;
|
|
601
|
+
let targetChange = changeName;
|
|
602
|
+
if (!targetChange) {
|
|
603
|
+
if (plan.changes.length === 0) {
|
|
604
|
+
throw new Error('No changes found in plan file. Cannot add tag without a target change.');
|
|
605
|
+
}
|
|
606
|
+
targetChange = plan.changes[plan.changes.length - 1].name;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
// Validate that the specified change exists
|
|
610
|
+
const changeExists = plan.changes.some(c => c.name === targetChange);
|
|
611
|
+
if (!changeExists) {
|
|
612
|
+
throw types_1.errors.CHANGE_NOT_FOUND({ change: targetChange });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Check if tag already exists
|
|
616
|
+
const existingTag = plan.tags.find(t => t.name === tagName);
|
|
617
|
+
if (existingTag) {
|
|
618
|
+
throw new Error(`Tag '${tagName}' already exists and points to change '${existingTag.change}'.`);
|
|
619
|
+
}
|
|
620
|
+
// Create new tag
|
|
621
|
+
const newTag = {
|
|
622
|
+
name: tagName,
|
|
623
|
+
change: targetChange,
|
|
624
|
+
timestamp: (0, generator_1.getNow)(),
|
|
625
|
+
planner: 'launchql',
|
|
626
|
+
email: 'launchql@5b0c196eeb62',
|
|
627
|
+
comment
|
|
628
|
+
};
|
|
629
|
+
plan.tags.push(newTag);
|
|
630
|
+
// Write updated plan file
|
|
631
|
+
(0, files_1.writePlanFile)(planPath, plan);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Add a change to the current module's plan file and create SQL files
|
|
635
|
+
*/
|
|
636
|
+
addChange(changeName, dependencies, comment) {
|
|
637
|
+
// Validate change name first
|
|
638
|
+
if (!changeName || !changeName.trim()) {
|
|
639
|
+
throw new Error('Change name is required');
|
|
640
|
+
}
|
|
641
|
+
if (!(0, validators_1.isValidChangeName)(changeName)) {
|
|
642
|
+
throw types_1.errors.INVALID_NAME({ name: changeName, type: 'change', rules: "Change names must follow Sqitch naming rules" });
|
|
643
|
+
}
|
|
644
|
+
if (!this.isInWorkspace() && !this.isInModule()) {
|
|
645
|
+
throw new Error('This command must be run inside a PGPM workspace or module.');
|
|
646
|
+
}
|
|
647
|
+
if (this.isInModule()) {
|
|
648
|
+
this.ensureModule();
|
|
649
|
+
if (!this.modulePath) {
|
|
650
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: 'module path', type: 'module' });
|
|
651
|
+
}
|
|
652
|
+
this.addChangeToModule(changeName, dependencies, comment);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
throw new Error('When running from workspace root, please specify --package or run from within a module directory.');
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Add change to the current module (internal helper)
|
|
659
|
+
*/
|
|
660
|
+
addChangeToModule(changeName, dependencies, comment) {
|
|
661
|
+
const planPath = path_1.default.join(this.modulePath, 'pgpm.plan');
|
|
662
|
+
// Parse existing plan file
|
|
663
|
+
const planResult = (0, parser_1.parsePlanFile)(planPath);
|
|
664
|
+
if (!planResult.data) {
|
|
665
|
+
throw types_1.errors.PLAN_PARSE_ERROR({ planPath, errors: planResult.errors.map(e => e.message).join(', ') });
|
|
666
|
+
}
|
|
667
|
+
const plan = planResult.data;
|
|
668
|
+
// Check if change already exists
|
|
669
|
+
const existingChange = plan.changes.find(c => c.name === changeName);
|
|
670
|
+
if (existingChange) {
|
|
671
|
+
throw new Error(`Change '${changeName}' already exists in plan.`);
|
|
672
|
+
}
|
|
673
|
+
// Validate dependencies exist if provided
|
|
674
|
+
if (dependencies && dependencies.length > 0) {
|
|
675
|
+
const currentPackage = plan.package;
|
|
676
|
+
for (const dep of dependencies) {
|
|
677
|
+
// Parse the dependency to check if it's a cross-module reference
|
|
678
|
+
const parsed = (0, validators_1.parseReference)(dep);
|
|
679
|
+
if (parsed && parsed.package && parsed.package !== currentPackage) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const depExists = plan.changes.some(c => c.name === dep);
|
|
683
|
+
if (!depExists) {
|
|
684
|
+
throw new Error(`Dependency '${dep}' not found in plan. Add dependencies before referencing them.`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Create new change
|
|
689
|
+
const newChange = {
|
|
690
|
+
name: changeName,
|
|
691
|
+
dependencies: dependencies || [],
|
|
692
|
+
timestamp: (0, generator_1.getNow)(),
|
|
693
|
+
planner: 'launchql',
|
|
694
|
+
email: 'launchql@5b0c196eeb62',
|
|
695
|
+
comment: comment || `add ${changeName}`
|
|
696
|
+
};
|
|
697
|
+
plan.changes.push(newChange);
|
|
698
|
+
// Write updated plan file
|
|
699
|
+
(0, files_1.writePlanFile)(planPath, plan);
|
|
700
|
+
// Create SQL files
|
|
701
|
+
this.createSqlFiles(changeName, dependencies || [], comment || `add ${changeName}`);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Create deploy/revert/verify SQL files for a change
|
|
705
|
+
*/
|
|
706
|
+
createSqlFiles(changeName, dependencies, comment) {
|
|
707
|
+
if (!this.modulePath) {
|
|
708
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: 'module path', type: 'module' });
|
|
709
|
+
}
|
|
710
|
+
const createdFiles = [];
|
|
711
|
+
const createSqlFile = (type, content) => {
|
|
712
|
+
const dir = path_1.default.dirname(changeName);
|
|
713
|
+
const fileName = path_1.default.basename(changeName);
|
|
714
|
+
const typeDir = path_1.default.join(this.modulePath, type);
|
|
715
|
+
const targetDir = path_1.default.join(typeDir, dir);
|
|
716
|
+
const filePath = path_1.default.join(targetDir, `${fileName}.sql`);
|
|
717
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
718
|
+
fs_1.default.writeFileSync(filePath, content);
|
|
719
|
+
// Track the relative path from module root
|
|
720
|
+
const relativePath = path_1.default.relative(this.modulePath, filePath);
|
|
721
|
+
createdFiles.push(relativePath);
|
|
722
|
+
};
|
|
723
|
+
// Create deploy file
|
|
724
|
+
const deployContent = `-- Deploy: ${changeName}
|
|
725
|
+
-- made with <3 @ constructive.io
|
|
726
|
+
|
|
727
|
+
${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join('\n') + '\n' : ''}
|
|
728
|
+
-- Add your deployment SQL here
|
|
729
|
+
`;
|
|
730
|
+
// Create revert file
|
|
731
|
+
const revertContent = `-- Revert: ${changeName}
|
|
732
|
+
|
|
733
|
+
-- Add your revert SQL here
|
|
734
|
+
`;
|
|
735
|
+
// Create verify file
|
|
736
|
+
const verifyContent = `-- Verify: ${changeName}
|
|
737
|
+
|
|
738
|
+
-- Add your verification SQL here
|
|
739
|
+
`;
|
|
740
|
+
createSqlFile('deploy', deployContent);
|
|
741
|
+
createSqlFile('revert', revertContent);
|
|
742
|
+
createSqlFile('verify', verifyContent);
|
|
743
|
+
// Log created files to stdout
|
|
744
|
+
process.stdout.write('\n✔ Files created\n\n');
|
|
745
|
+
createdFiles.forEach(file => {
|
|
746
|
+
process.stdout.write(` create ${file}\n`);
|
|
747
|
+
});
|
|
748
|
+
process.stdout.write('\n✨ All set!\n\n');
|
|
749
|
+
}
|
|
750
|
+
// ──────────────── Packaging and npm ────────────────
|
|
751
|
+
publishToDist(distFolder = 'dist') {
|
|
752
|
+
this.ensureModule();
|
|
753
|
+
const modPath = this.modulePath; // use modulePath, not cwd
|
|
754
|
+
const name = this.getModuleName();
|
|
755
|
+
const controlFile = `${name}.control`;
|
|
756
|
+
const fullDist = path_1.default.join(modPath, distFolder);
|
|
757
|
+
if (fs_1.default.existsSync(fullDist)) {
|
|
758
|
+
fs_1.default.rmSync(fullDist, { recursive: true, force: true });
|
|
759
|
+
}
|
|
760
|
+
fs_1.default.mkdirSync(fullDist, { recursive: true });
|
|
761
|
+
const folders = ['deploy', 'revert', 'sql', 'verify'];
|
|
762
|
+
const files = ['Makefile', 'package.json', 'pgpm.plan', controlFile];
|
|
763
|
+
// Add README file regardless of casing
|
|
764
|
+
const readmeFile = fs_1.default.readdirSync(modPath).find(f => /^readme\.md$/i.test(f));
|
|
765
|
+
if (readmeFile) {
|
|
766
|
+
files.push(readmeFile); // Include it in the list of files to copy
|
|
767
|
+
}
|
|
768
|
+
for (const folder of folders) {
|
|
769
|
+
const src = path_1.default.join(modPath, folder);
|
|
770
|
+
if (fs_1.default.existsSync(src)) {
|
|
771
|
+
fs_1.default.cpSync(src, path_1.default.join(fullDist, folder), { recursive: true });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
for (const file of files) {
|
|
775
|
+
const src = path_1.default.join(modPath, file);
|
|
776
|
+
if (!fs_1.default.existsSync(src)) {
|
|
777
|
+
throw new Error(`Missing required file: ${file}`);
|
|
778
|
+
}
|
|
779
|
+
fs_1.default.cpSync(src, path_1.default.join(fullDist, file));
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Installs an extension npm package into the local skitch extensions directory,
|
|
784
|
+
* and automatically adds it to the current module’s package.json dependencies.
|
|
785
|
+
*/
|
|
786
|
+
async installModules(...pkgstrs) {
|
|
787
|
+
this.ensureWorkspace();
|
|
788
|
+
this.ensureModule();
|
|
789
|
+
const originalDir = process.cwd();
|
|
790
|
+
const skitchExtDir = path_1.default.join(this.workspacePath, 'extensions');
|
|
791
|
+
const pkgJsonPath = path_1.default.join(this.modulePath, 'package.json');
|
|
792
|
+
if (!fs_1.default.existsSync(pkgJsonPath)) {
|
|
793
|
+
throw new Error(`No package.json found at module path: ${this.modulePath}`);
|
|
794
|
+
}
|
|
795
|
+
const pkgData = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
796
|
+
pkgData.dependencies = pkgData.dependencies || {};
|
|
797
|
+
const newlyAdded = [];
|
|
798
|
+
for (const pkgstr of pkgstrs) {
|
|
799
|
+
const { name } = (0, parse_package_name_1.parse)(pkgstr);
|
|
800
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'lql-install-'));
|
|
801
|
+
try {
|
|
802
|
+
process.chdir(tempDir);
|
|
803
|
+
(0, child_process_1.execSync)(`npm install ${pkgstr} --production --prefix ./extensions`, {
|
|
804
|
+
stdio: 'inherit'
|
|
805
|
+
});
|
|
806
|
+
const matches = glob.sync('./extensions/**/pgpm.plan');
|
|
807
|
+
const installs = matches.map((conf) => {
|
|
808
|
+
const fullConf = (0, path_1.resolve)(conf);
|
|
809
|
+
const extDir = (0, path_1.dirname)(fullConf);
|
|
810
|
+
const relativeDir = extDir.split('node_modules/')[1];
|
|
811
|
+
const dstDir = path_1.default.join(skitchExtDir, relativeDir);
|
|
812
|
+
return { src: extDir, dst: dstDir, pkg: relativeDir };
|
|
813
|
+
});
|
|
814
|
+
for (const { src, dst, pkg } of installs) {
|
|
815
|
+
if (fs_1.default.existsSync(dst)) {
|
|
816
|
+
fs_1.default.rmSync(dst, { recursive: true, force: true });
|
|
817
|
+
}
|
|
818
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
|
|
819
|
+
(0, child_process_1.execSync)(`mv "${src}" "${dst}"`);
|
|
820
|
+
logger.success(`✔ installed ${pkg}`);
|
|
821
|
+
const pkgJsonFile = path_1.default.join(dst, 'package.json');
|
|
822
|
+
if (!fs_1.default.existsSync(pkgJsonFile)) {
|
|
823
|
+
throw new Error(`Missing package.json in installed extension: ${dst}`);
|
|
824
|
+
}
|
|
825
|
+
const { version } = JSON.parse(fs_1.default.readFileSync(pkgJsonFile, 'utf-8'));
|
|
826
|
+
pkgData.dependencies[name] = `${version}`;
|
|
827
|
+
const extensionName = (0, files_2.getExtensionName)(dst);
|
|
828
|
+
newlyAdded.push(extensionName);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
833
|
+
process.chdir(originalDir);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const { dependencies, devDependencies, ...rest } = pkgData;
|
|
837
|
+
const finalPkgData = { ...rest };
|
|
838
|
+
if (dependencies) {
|
|
839
|
+
finalPkgData.dependencies = sortObjectByKey(dependencies);
|
|
840
|
+
}
|
|
841
|
+
if (devDependencies) {
|
|
842
|
+
finalPkgData.devDependencies = sortObjectByKey(devDependencies);
|
|
843
|
+
}
|
|
844
|
+
fs_1.default.writeFileSync(pkgJsonPath, JSON.stringify(finalPkgData, null, 2));
|
|
845
|
+
logger.success(`📦 Updated package.json with: ${pkgstrs.join(', ')}`);
|
|
846
|
+
// ─── Update .control file with actual extension names ──────────────
|
|
847
|
+
const currentDeps = this.getRequiredModules();
|
|
848
|
+
const updatedDeps = Array.from(new Set([...currentDeps, ...newlyAdded])).sort();
|
|
849
|
+
(0, files_2.writeExtensions)(this.modulePath, updatedDeps);
|
|
850
|
+
}
|
|
851
|
+
// ──────────────── Package Operations ────────────────
|
|
852
|
+
/**
|
|
853
|
+
* Get the set of modules that have been deployed to the database
|
|
854
|
+
*/
|
|
855
|
+
async getDeployedModules(pgConfig) {
|
|
856
|
+
try {
|
|
857
|
+
const client = new client_1.PgpmMigrate(pgConfig);
|
|
858
|
+
await client.initialize();
|
|
859
|
+
const status = await client.status();
|
|
860
|
+
return new Set(status.map(s => s.package));
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
if (error.code === '42P01' || error.code === '3F000') {
|
|
864
|
+
return new Set();
|
|
865
|
+
}
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async resolveWorkspaceExtensionDependencies(opts) {
|
|
870
|
+
const modules = this.getModuleMap();
|
|
871
|
+
const allModuleNames = Object.keys(modules);
|
|
872
|
+
if (allModuleNames.length === 0) {
|
|
873
|
+
return { resolved: [], external: [] };
|
|
874
|
+
}
|
|
875
|
+
// Create a virtual module that depends on all workspace modules
|
|
876
|
+
const virtualModuleName = '_virtual/workspace';
|
|
877
|
+
const virtualModuleMap = {
|
|
878
|
+
...modules,
|
|
879
|
+
[virtualModuleName]: {
|
|
880
|
+
requires: allModuleNames
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
const { resolved, external } = (0, deps_1.resolveExtensionDependencies)(virtualModuleName, virtualModuleMap);
|
|
884
|
+
let filteredResolved = resolved.filter((moduleName) => moduleName !== virtualModuleName);
|
|
885
|
+
// Filter by deployment status if requested
|
|
886
|
+
if (opts?.filterDeployed && opts?.pgConfig) {
|
|
887
|
+
const deployedModules = await this.getDeployedModules(opts.pgConfig);
|
|
888
|
+
filteredResolved = filteredResolved.filter(module => deployedModules.has(module));
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
resolved: filteredResolved,
|
|
892
|
+
external: external
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
parsePackageTarget(target) {
|
|
896
|
+
let name;
|
|
897
|
+
let toChange;
|
|
898
|
+
if (!target) {
|
|
899
|
+
const context = this.getContext();
|
|
900
|
+
if (context === PackageContext.Module || context === PackageContext.ModuleInsideWorkspace) {
|
|
901
|
+
name = this.getModuleName();
|
|
902
|
+
}
|
|
903
|
+
else if (context === PackageContext.Workspace) {
|
|
904
|
+
const modules = this.getModuleMap();
|
|
905
|
+
const moduleNames = Object.keys(modules);
|
|
906
|
+
if (moduleNames.length === 0) {
|
|
907
|
+
throw new Error('No modules found in workspace');
|
|
908
|
+
}
|
|
909
|
+
name = null; // Indicates workspace-wide operation
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
throw new Error('Not in a PGPM workspace or module');
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
const parsed = (0, target_utils_1.parseTarget)(target);
|
|
917
|
+
name = parsed.packageName;
|
|
918
|
+
toChange = parsed.toChange;
|
|
919
|
+
}
|
|
920
|
+
return { name, toChange };
|
|
921
|
+
}
|
|
922
|
+
async deploy(opts, target, recursive = true) {
|
|
923
|
+
const log = new logger_1.Logger('deploy');
|
|
924
|
+
const { name, toChange } = this.parsePackageTarget(target);
|
|
925
|
+
if (recursive) {
|
|
926
|
+
// Cache for fast deployment
|
|
927
|
+
const deployFastCache = {};
|
|
928
|
+
const getCacheKey = (pg, name, database) => {
|
|
929
|
+
const { host, port, user } = pg ?? {};
|
|
930
|
+
return `${host}:${port}:${user}:${database}:${name}`;
|
|
931
|
+
};
|
|
932
|
+
const modules = this.getModuleMap();
|
|
933
|
+
let extensions;
|
|
934
|
+
if (name === null) {
|
|
935
|
+
// When name is null, deploy ALL modules in the workspace
|
|
936
|
+
extensions = await this.resolveWorkspaceExtensionDependencies();
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
const moduleProject = this.getModuleProject(name);
|
|
940
|
+
extensions = moduleProject.getModuleExtensions();
|
|
941
|
+
}
|
|
942
|
+
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
943
|
+
const targetDescription = name === null ? 'all modules' : name;
|
|
944
|
+
log.success(`🚀 Starting deployment to database ${opts.pg.database}...`);
|
|
945
|
+
for (const extension of extensions.resolved) {
|
|
946
|
+
try {
|
|
947
|
+
if (extensions.external.includes(extension)) {
|
|
948
|
+
const msg = `CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;`;
|
|
949
|
+
log.info(`📥 Installing external extension: ${extension}`);
|
|
950
|
+
await pgPool.query(msg);
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
|
|
954
|
+
log.info(`📂 Deploying local module: ${extension}`);
|
|
955
|
+
if (opts.deployment.fast) {
|
|
956
|
+
const localProject = this.getModuleProject(extension);
|
|
957
|
+
const cacheKey = getCacheKey(opts.pg, extension, opts.pg.database);
|
|
958
|
+
if (opts.deployment.cache && deployFastCache[cacheKey]) {
|
|
959
|
+
log.warn(`⚡ Using cached pkg for ${extension}.`);
|
|
960
|
+
await pgPool.query(deployFastCache[cacheKey].sql);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
let pkg;
|
|
964
|
+
try {
|
|
965
|
+
pkg = await (0, package_1.packageModule)(localProject.modulePath, {
|
|
966
|
+
usePlan: opts.deployment.usePlan,
|
|
967
|
+
extension: false
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
const errorLines = [];
|
|
972
|
+
errorLines.push(`❌ Failed to package module "${extension}" at path: ${modulePath}`);
|
|
973
|
+
errorLines.push(` Module Path: ${modulePath}`);
|
|
974
|
+
errorLines.push(` Workspace Path: ${this.workspacePath}`);
|
|
975
|
+
errorLines.push(` Error Code: ${err.code || 'N/A'}`);
|
|
976
|
+
errorLines.push(` Error Message: ${err.message || 'Unknown error'}`);
|
|
977
|
+
if (err.code === 'ENOENT') {
|
|
978
|
+
errorLines.push('💡 Hint: File or directory not found. Check if the module path is correct.');
|
|
979
|
+
}
|
|
980
|
+
else if (err.code === 'EACCES') {
|
|
981
|
+
errorLines.push('💡 Hint: Permission denied. Check file permissions.');
|
|
982
|
+
}
|
|
983
|
+
else if (err.message && err.message.includes('pgpm.plan')) {
|
|
984
|
+
errorLines.push('💡 Hint: pgpm.plan file issue. Check if the plan file exists and is valid.');
|
|
985
|
+
}
|
|
986
|
+
log.error(errorLines.join('\n'));
|
|
987
|
+
console.error(err);
|
|
988
|
+
throw types_1.errors.DEPLOYMENT_FAILED({
|
|
989
|
+
type: 'Deployment',
|
|
990
|
+
module: extension
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
await pgPool.query(pkg.sql);
|
|
994
|
+
if (opts.deployment.cache) {
|
|
995
|
+
deployFastCache[cacheKey] = pkg;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
try {
|
|
1000
|
+
const client = new client_1.PgpmMigrate(opts.pg);
|
|
1001
|
+
// Only apply toChange to the target module, not its dependencies
|
|
1002
|
+
const moduleToChange = extension === name ? toChange : undefined;
|
|
1003
|
+
const result = await client.deploy({
|
|
1004
|
+
modulePath,
|
|
1005
|
+
toChange: moduleToChange,
|
|
1006
|
+
useTransaction: opts.deployment.useTx,
|
|
1007
|
+
logOnly: opts.deployment.logOnly,
|
|
1008
|
+
usePlan: opts.deployment.usePlan
|
|
1009
|
+
});
|
|
1010
|
+
if (result.failed) {
|
|
1011
|
+
throw types_1.errors.OPERATION_FAILED({ operation: 'Deployment', target: result.failed });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch (deployError) {
|
|
1015
|
+
log.error(`❌ Deployment failed for module ${extension}`);
|
|
1016
|
+
console.error(deployError);
|
|
1017
|
+
throw types_1.errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
catch (err) {
|
|
1023
|
+
log.error(`🛑 Error during deployment: ${err instanceof Error ? err.message : err}`);
|
|
1024
|
+
console.error(err);
|
|
1025
|
+
throw types_1.errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
log.success(`✅ Deployment complete for ${targetDescription}.`);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
if (name === null) {
|
|
1032
|
+
throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'deployment' });
|
|
1033
|
+
}
|
|
1034
|
+
const moduleProject = this.getModuleProject(name);
|
|
1035
|
+
const modulePath = moduleProject.getModulePath();
|
|
1036
|
+
if (!modulePath) {
|
|
1037
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
|
|
1038
|
+
}
|
|
1039
|
+
const client = new client_1.PgpmMigrate(opts.pg);
|
|
1040
|
+
const result = await client.deploy({
|
|
1041
|
+
modulePath,
|
|
1042
|
+
toChange,
|
|
1043
|
+
useTransaction: opts.deployment?.useTx,
|
|
1044
|
+
logOnly: opts.deployment?.logOnly,
|
|
1045
|
+
usePlan: opts.deployment?.usePlan
|
|
1046
|
+
});
|
|
1047
|
+
if (result.failed) {
|
|
1048
|
+
throw types_1.errors.OPERATION_FAILED({ operation: 'Deployment', target: result.failed });
|
|
1049
|
+
}
|
|
1050
|
+
log.success(`✅ Single module deployment complete for ${name}.`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Reverts database changes for modules. Unlike verify operations, revert operations
|
|
1055
|
+
* modify database state and must ensure dependent modules are reverted before their
|
|
1056
|
+
* dependencies to prevent database constraint violations.
|
|
1057
|
+
*/
|
|
1058
|
+
async revert(opts, target, recursive = true) {
|
|
1059
|
+
const log = new logger_1.Logger('revert');
|
|
1060
|
+
const { name, toChange } = this.parsePackageTarget(target);
|
|
1061
|
+
if (recursive) {
|
|
1062
|
+
const modules = this.getModuleMap();
|
|
1063
|
+
// Mirror deploy logic: find all modules that depend on the target module
|
|
1064
|
+
let extensionsToRevert;
|
|
1065
|
+
if (name === null) {
|
|
1066
|
+
// When name is null, revert ALL deployed modules in the workspace
|
|
1067
|
+
extensionsToRevert = await this.resolveWorkspaceExtensionDependencies({
|
|
1068
|
+
filterDeployed: true,
|
|
1069
|
+
pgConfig: opts.pg
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
// Always use workspace-wide resolution in recursive mode, but filter to deployed modules
|
|
1074
|
+
const workspaceExtensions = await this.resolveWorkspaceExtensionDependencies({
|
|
1075
|
+
filterDeployed: true,
|
|
1076
|
+
pgConfig: opts.pg
|
|
1077
|
+
});
|
|
1078
|
+
extensionsToRevert = truncateExtensionsToTarget(workspaceExtensions, name);
|
|
1079
|
+
}
|
|
1080
|
+
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
1081
|
+
const targetDescription = name === null ? 'all modules' : name;
|
|
1082
|
+
log.success(`🧹 Starting revert process on database ${opts.pg.database}...`);
|
|
1083
|
+
const reversedExtensions = [...extensionsToRevert.resolved].reverse();
|
|
1084
|
+
for (const extension of reversedExtensions) {
|
|
1085
|
+
try {
|
|
1086
|
+
if (extensionsToRevert.external.includes(extension)) {
|
|
1087
|
+
const msg = `DROP EXTENSION IF EXISTS "${extension}" RESTRICT;`;
|
|
1088
|
+
log.warn(`⚠️ Dropping external extension: ${extension}`);
|
|
1089
|
+
try {
|
|
1090
|
+
await pgPool.query(msg);
|
|
1091
|
+
}
|
|
1092
|
+
catch (err) {
|
|
1093
|
+
if (err.code === '2BP01') {
|
|
1094
|
+
log.warn(`⚠️ Cannot drop extension ${extension} due to dependencies, skipping`);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
throw err;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
|
|
1103
|
+
log.info(`📂 Reverting local module: ${extension}`);
|
|
1104
|
+
try {
|
|
1105
|
+
const client = new client_1.PgpmMigrate(opts.pg);
|
|
1106
|
+
// Only apply toChange to the target module, not its dependencies
|
|
1107
|
+
const moduleToChange = extension === name ? toChange : undefined;
|
|
1108
|
+
const result = await client.revert({
|
|
1109
|
+
modulePath,
|
|
1110
|
+
toChange: moduleToChange,
|
|
1111
|
+
useTransaction: opts.deployment.useTx
|
|
1112
|
+
});
|
|
1113
|
+
if (result.failed) {
|
|
1114
|
+
throw types_1.errors.OPERATION_FAILED({ operation: 'Revert', target: result.failed });
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch (revertError) {
|
|
1118
|
+
log.error(`❌ Revert failed for module ${extension}`);
|
|
1119
|
+
throw types_1.errors.DEPLOYMENT_FAILED({ type: 'Revert', module: extension });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
catch (e) {
|
|
1124
|
+
log.error(`🛑 Error during revert: ${e instanceof Error ? e.message : e}`);
|
|
1125
|
+
console.error(e);
|
|
1126
|
+
throw types_1.errors.DEPLOYMENT_FAILED({ type: 'Revert', module: extension });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
log.success(`✅ Revert complete for ${targetDescription}.`);
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
if (name === null) {
|
|
1133
|
+
throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'revert' });
|
|
1134
|
+
}
|
|
1135
|
+
const moduleProject = this.getModuleProject(name);
|
|
1136
|
+
const modulePath = moduleProject.getModulePath();
|
|
1137
|
+
if (!modulePath) {
|
|
1138
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
|
|
1139
|
+
}
|
|
1140
|
+
const client = new client_1.PgpmMigrate(opts.pg);
|
|
1141
|
+
const result = await client.revert({
|
|
1142
|
+
modulePath,
|
|
1143
|
+
toChange,
|
|
1144
|
+
useTransaction: opts.deployment?.useTx
|
|
1145
|
+
});
|
|
1146
|
+
if (result.failed) {
|
|
1147
|
+
throw types_1.errors.OPERATION_FAILED({ operation: 'Revert', target: result.failed });
|
|
1148
|
+
}
|
|
1149
|
+
log.success(`✅ Single module revert complete for ${name}.`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async verify(opts, target, recursive = true) {
|
|
1153
|
+
const log = new logger_1.Logger('verify');
|
|
1154
|
+
const { name, toChange } = this.parsePackageTarget(target);
|
|
1155
|
+
if (recursive) {
|
|
1156
|
+
const modules = this.getModuleMap();
|
|
1157
|
+
let extensions;
|
|
1158
|
+
if (name === null) {
|
|
1159
|
+
// When name is null, verify ALL modules in the workspace
|
|
1160
|
+
extensions = await this.resolveWorkspaceExtensionDependencies();
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
const moduleProject = this.getModuleProject(name);
|
|
1164
|
+
extensions = moduleProject.getModuleExtensions();
|
|
1165
|
+
}
|
|
1166
|
+
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
1167
|
+
const targetDescription = name === null ? 'all modules' : name;
|
|
1168
|
+
log.success(`🔎 Verifying deployment of ${targetDescription} on database ${opts.pg.database}...`);
|
|
1169
|
+
for (const extension of extensions.resolved) {
|
|
1170
|
+
try {
|
|
1171
|
+
if (extensions.external.includes(extension)) {
|
|
1172
|
+
const query = `SELECT 1/count(*) FROM pg_available_extensions WHERE name = $1`;
|
|
1173
|
+
log.info(`🔍 Verifying external extension: ${extension}`);
|
|
1174
|
+
await pgPool.query(query, [extension]);
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
|
|
1178
|
+
log.info(`📂 Verifying local module: ${extension}`);
|
|
1179
|
+
try {
|
|
1180
|
+
const client = new client_1.PgpmMigrate(opts.pg);
|
|
1181
|
+
// Only apply toChange to the target module, not its dependencies
|
|
1182
|
+
const moduleToChange = extension === name ? toChange : undefined;
|
|
1183
|
+
const result = await client.verify({
|
|
1184
|
+
modulePath,
|
|
1185
|
+
toChange: moduleToChange
|
|
1186
|
+
});
|
|
1187
|
+
if (result.failed.length > 0) {
|
|
1188
|
+
throw types_1.errors.OPERATION_FAILED({ operation: 'Verification', reason: `${result.failed.length} changes: ${result.failed.join(', ')}` });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
catch (verifyError) {
|
|
1192
|
+
log.error(`❌ Verification failed for module ${extension}`);
|
|
1193
|
+
throw types_1.errors.DEPLOYMENT_FAILED({ type: 'Verify', module: extension });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch (e) {
|
|
1198
|
+
log.error(`🛑 Error during verification: ${e instanceof Error ? e.message : e}`);
|
|
1199
|
+
console.error(e);
|
|
1200
|
+
throw types_1.errors.DEPLOYMENT_FAILED({ type: 'Verify', module: extension });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
log.success(`✅ Verification complete for ${targetDescription}.`);
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
if (name === null) {
|
|
1207
|
+
throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'verification' });
|
|
1208
|
+
}
|
|
1209
|
+
const moduleProject = this.getModuleProject(name);
|
|
1210
|
+
const modulePath = moduleProject.getModulePath();
|
|
1211
|
+
if (!modulePath) {
|
|
1212
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
|
|
1213
|
+
}
|
|
1214
|
+
const client = new client_1.PgpmMigrate(opts.pg);
|
|
1215
|
+
const result = await client.verify({
|
|
1216
|
+
modulePath,
|
|
1217
|
+
toChange
|
|
1218
|
+
});
|
|
1219
|
+
if (result.failed.length > 0) {
|
|
1220
|
+
throw types_1.errors.OPERATION_FAILED({ operation: 'Verification', reason: `${result.failed.length} changes: ${result.failed.join(', ')}` });
|
|
1221
|
+
}
|
|
1222
|
+
log.success(`✅ Single module verification complete for ${name}.`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async removeFromPlan(toChange) {
|
|
1226
|
+
const log = new logger_1.Logger('remove');
|
|
1227
|
+
const modulePath = this.getModulePath();
|
|
1228
|
+
if (!modulePath) {
|
|
1229
|
+
throw types_1.errors.PATH_NOT_FOUND({ path: 'module path', type: 'module' });
|
|
1230
|
+
}
|
|
1231
|
+
const planPath = path_1.default.join(modulePath, 'pgpm.plan');
|
|
1232
|
+
const result = (0, parser_1.parsePlanFile)(planPath);
|
|
1233
|
+
if (result.errors.length > 0) {
|
|
1234
|
+
throw types_1.errors.PLAN_PARSE_ERROR({ planPath, errors: result.errors.map(e => e.message).join(', ') });
|
|
1235
|
+
}
|
|
1236
|
+
const plan = result.data;
|
|
1237
|
+
if (toChange.startsWith('@')) {
|
|
1238
|
+
const tagName = toChange.substring(1); // Remove the '@' prefix
|
|
1239
|
+
const tagToRemove = plan.tags.find(tag => tag.name === tagName);
|
|
1240
|
+
if (!tagToRemove) {
|
|
1241
|
+
throw types_1.errors.TAG_NOT_FOUND({ tag: toChange });
|
|
1242
|
+
}
|
|
1243
|
+
const tagChangeIndex = plan.changes.findIndex(c => c.name === tagToRemove.change);
|
|
1244
|
+
if (tagChangeIndex === -1) {
|
|
1245
|
+
throw types_1.errors.CHANGE_NOT_FOUND({ change: tagToRemove.change, plan: `for tag '${toChange}'` });
|
|
1246
|
+
}
|
|
1247
|
+
const changesToRemove = plan.changes.slice(tagChangeIndex);
|
|
1248
|
+
plan.changes = plan.changes.slice(0, tagChangeIndex);
|
|
1249
|
+
plan.tags = plan.tags.filter(tag => tag.name !== tagName && !changesToRemove.some(change => change.name === tag.change));
|
|
1250
|
+
for (const change of changesToRemove) {
|
|
1251
|
+
for (const scriptType of ['deploy', 'revert', 'verify']) {
|
|
1252
|
+
const scriptPath = path_1.default.join(modulePath, scriptType, `${change.name}.sql`);
|
|
1253
|
+
if (fs_1.default.existsSync(scriptPath)) {
|
|
1254
|
+
fs_1.default.unlinkSync(scriptPath);
|
|
1255
|
+
log.info(`Deleted ${scriptType}/${change.name}.sql`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// Write updated plan file
|
|
1260
|
+
(0, files_1.writePlanFile)(planPath, plan);
|
|
1261
|
+
log.success(`Removed tag ${toChange} and ${changesToRemove.length} subsequent changes from plan`);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const targetIndex = plan.changes.findIndex(c => c.name === toChange);
|
|
1265
|
+
if (targetIndex === -1) {
|
|
1266
|
+
throw types_1.errors.CHANGE_NOT_FOUND({ change: toChange });
|
|
1267
|
+
}
|
|
1268
|
+
const changesToRemove = plan.changes.slice(targetIndex);
|
|
1269
|
+
plan.changes = plan.changes.slice(0, targetIndex);
|
|
1270
|
+
plan.tags = plan.tags.filter(tag => !changesToRemove.some(change => change.name === tag.change));
|
|
1271
|
+
for (const change of changesToRemove) {
|
|
1272
|
+
for (const scriptType of ['deploy', 'revert', 'verify']) {
|
|
1273
|
+
const scriptPath = path_1.default.join(modulePath, scriptType, `${change.name}.sql`);
|
|
1274
|
+
if (fs_1.default.existsSync(scriptPath)) {
|
|
1275
|
+
fs_1.default.unlinkSync(scriptPath);
|
|
1276
|
+
log.info(`Deleted ${scriptType}/${change.name}.sql`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Write updated plan file
|
|
1281
|
+
(0, files_1.writePlanFile)(planPath, plan);
|
|
1282
|
+
log.success(`Removed ${changesToRemove.length} changes from plan`);
|
|
1283
|
+
}
|
|
1284
|
+
analyzeModule() {
|
|
1285
|
+
this.ensureModule();
|
|
1286
|
+
const info = this.getModuleInfo();
|
|
1287
|
+
const modPath = this.getModulePath();
|
|
1288
|
+
const issues = [];
|
|
1289
|
+
const exists = (p) => fs_1.default.existsSync(p);
|
|
1290
|
+
const read = (p) => (exists(p) ? fs_1.default.readFileSync(p, 'utf8') : undefined);
|
|
1291
|
+
const planPath = path_1.default.join(modPath, 'pgpm.plan');
|
|
1292
|
+
if (!exists(planPath))
|
|
1293
|
+
issues.push({ code: 'missing_plan', message: 'Missing pgpm.plan', file: planPath });
|
|
1294
|
+
const pkgJsonPath = path_1.default.join(modPath, 'package.json');
|
|
1295
|
+
if (!exists(pkgJsonPath))
|
|
1296
|
+
issues.push({ code: 'missing_package_json', message: 'Missing package.json', file: pkgJsonPath });
|
|
1297
|
+
const makefilePath = info.Makefile;
|
|
1298
|
+
if (!exists(makefilePath))
|
|
1299
|
+
issues.push({ code: 'missing_makefile', message: 'Missing Makefile', file: makefilePath });
|
|
1300
|
+
const controlPath = info.controlFile;
|
|
1301
|
+
if (!exists(controlPath))
|
|
1302
|
+
issues.push({ code: 'missing_control', message: 'Missing control file', file: controlPath });
|
|
1303
|
+
const sqlCombined = info.sqlFile ? path_1.default.join(modPath, info.sqlFile) : path_1.default.join(modPath, 'sql', `${info.extname}--${info.version}.sql`);
|
|
1304
|
+
if (!exists(sqlCombined))
|
|
1305
|
+
issues.push({ code: 'missing_sql', message: 'Missing combined sql file', file: sqlCombined });
|
|
1306
|
+
const deployDir = path_1.default.join(modPath, 'deploy');
|
|
1307
|
+
if (!exists(deployDir))
|
|
1308
|
+
issues.push({ code: 'missing_deploy_dir', message: 'Missing deploy directory', file: deployDir });
|
|
1309
|
+
const revertDir = path_1.default.join(modPath, 'revert');
|
|
1310
|
+
if (!exists(revertDir))
|
|
1311
|
+
issues.push({ code: 'missing_revert_dir', message: 'Missing revert directory', file: revertDir });
|
|
1312
|
+
const verifyDir = path_1.default.join(modPath, 'verify');
|
|
1313
|
+
if (!exists(verifyDir))
|
|
1314
|
+
issues.push({ code: 'missing_verify_dir', message: 'Missing verify directory', file: verifyDir });
|
|
1315
|
+
if (exists(planPath)) {
|
|
1316
|
+
try {
|
|
1317
|
+
const parsed = (0, parser_1.parsePlanFile)(planPath);
|
|
1318
|
+
const pkgName = parsed.data?.package;
|
|
1319
|
+
if (!pkgName)
|
|
1320
|
+
issues.push({ code: 'plan_missing_project', message: '%project missing', file: planPath });
|
|
1321
|
+
if (pkgName && pkgName !== info.extname)
|
|
1322
|
+
issues.push({ code: 'plan_project_mismatch', message: `pgpm.plan %project ${pkgName} != ${info.extname}`, file: planPath });
|
|
1323
|
+
const uri = parsed.data?.uri;
|
|
1324
|
+
if (uri && uri !== info.extname)
|
|
1325
|
+
issues.push({ code: 'plan_uri_mismatch', message: `pgpm.plan %uri ${uri} != ${info.extname}`, file: planPath });
|
|
1326
|
+
}
|
|
1327
|
+
catch (e) {
|
|
1328
|
+
issues.push({ code: 'plan_parse_error', message: e?.message || 'Plan parse error', file: planPath });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (exists(makefilePath)) {
|
|
1332
|
+
const mf = read(makefilePath) || '';
|
|
1333
|
+
const extMatch = mf.match(/^EXTENSION\s*=\s*(.+)$/m);
|
|
1334
|
+
const dataMatch = mf.match(/^DATA\s*=\s*sql\/(.+)\.sql$/m);
|
|
1335
|
+
if (!extMatch)
|
|
1336
|
+
issues.push({ code: 'makefile_missing_extension', message: 'Makefile missing EXTENSION', file: makefilePath });
|
|
1337
|
+
if (!dataMatch)
|
|
1338
|
+
issues.push({ code: 'makefile_missing_data', message: 'Makefile missing DATA', file: makefilePath });
|
|
1339
|
+
if (extMatch && extMatch[1].trim() !== info.extname)
|
|
1340
|
+
issues.push({ code: 'makefile_extension_mismatch', message: `Makefile EXTENSION ${extMatch[1].trim()} != ${info.extname}`, file: makefilePath });
|
|
1341
|
+
const expectedData = `${info.extname}--${info.version}`;
|
|
1342
|
+
if (dataMatch && dataMatch[1].trim() !== expectedData)
|
|
1343
|
+
issues.push({ code: 'makefile_data_mismatch', message: `Makefile DATA sql/${dataMatch[1].trim()}.sql != sql/${expectedData}.sql`, file: makefilePath });
|
|
1344
|
+
}
|
|
1345
|
+
if (exists(controlPath)) {
|
|
1346
|
+
const base = path_1.default.basename(controlPath);
|
|
1347
|
+
const expected = `${info.extname}.control`;
|
|
1348
|
+
if (base !== expected)
|
|
1349
|
+
issues.push({ code: 'control_filename_mismatch', message: `Control filename ${base} != ${expected}`, file: controlPath });
|
|
1350
|
+
}
|
|
1351
|
+
return { ok: issues.length === 0, name: info.extname, path: modPath, issues };
|
|
1352
|
+
}
|
|
1353
|
+
renameModule(newName, opts) {
|
|
1354
|
+
this.ensureModule();
|
|
1355
|
+
const info = this.getModuleInfo();
|
|
1356
|
+
const modPath = this.getModulePath();
|
|
1357
|
+
const changed = [];
|
|
1358
|
+
const warnings = [];
|
|
1359
|
+
const dry = !!opts?.dryRun;
|
|
1360
|
+
const valid = /^[a-z][a-z0-9_]*$/;
|
|
1361
|
+
if (!valid.test(newName)) {
|
|
1362
|
+
throw types_1.errors.INVALID_NAME({ name: newName, type: 'module', rules: 'lowercase letters, digits, underscores; must start with letter' });
|
|
1363
|
+
}
|
|
1364
|
+
const planPath = path_1.default.join(modPath, 'pgpm.plan');
|
|
1365
|
+
if (fs_1.default.existsSync(planPath)) {
|
|
1366
|
+
try {
|
|
1367
|
+
const parsed = (0, parser_1.parsePlanFile)(planPath);
|
|
1368
|
+
if (parsed.data) {
|
|
1369
|
+
parsed.data.package = newName;
|
|
1370
|
+
parsed.data.uri = newName;
|
|
1371
|
+
if (!dry)
|
|
1372
|
+
(0, files_1.writePlanFile)(planPath, parsed.data);
|
|
1373
|
+
changed.push(planPath);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
catch (e) {
|
|
1377
|
+
warnings.push(`failed to update pgpm.plan`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
warnings.push('missing pgpm.plan');
|
|
1382
|
+
}
|
|
1383
|
+
const pkgJsonPath = path_1.default.join(modPath, 'package.json');
|
|
1384
|
+
if (fs_1.default.existsSync(pkgJsonPath) && opts?.syncPackageJsonName) {
|
|
1385
|
+
try {
|
|
1386
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf8'));
|
|
1387
|
+
const oldName = pkg.name;
|
|
1388
|
+
if (oldName) {
|
|
1389
|
+
if (oldName.startsWith('@')) {
|
|
1390
|
+
const parts = oldName.split('/');
|
|
1391
|
+
if (parts.length === 2)
|
|
1392
|
+
pkg.name = `${parts[0]}/${newName}`;
|
|
1393
|
+
else
|
|
1394
|
+
pkg.name = newName;
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
pkg.name = newName;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
pkg.name = newName;
|
|
1402
|
+
}
|
|
1403
|
+
if (!dry)
|
|
1404
|
+
fs_1.default.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
1405
|
+
changed.push(pkgJsonPath);
|
|
1406
|
+
}
|
|
1407
|
+
catch {
|
|
1408
|
+
warnings.push('failed to update package.json name');
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const oldControl = info.controlFile;
|
|
1412
|
+
const newControl = path_1.default.join(modPath, `${newName}.control`);
|
|
1413
|
+
const version = info.version;
|
|
1414
|
+
const requires = (() => {
|
|
1415
|
+
try {
|
|
1416
|
+
const c = fs_1.default.readFileSync(oldControl, 'utf8');
|
|
1417
|
+
const line = c.split('\n').find(l => /^requires/.test(l));
|
|
1418
|
+
if (!line)
|
|
1419
|
+
return [];
|
|
1420
|
+
return line.split('=')[1].split("'")[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
return [];
|
|
1424
|
+
}
|
|
1425
|
+
})();
|
|
1426
|
+
if (fs_1.default.existsSync(oldControl)) {
|
|
1427
|
+
if (!dry) {
|
|
1428
|
+
const content = (0, writer_1.generateControlFileContent)({ name: newName, version, requires });
|
|
1429
|
+
fs_1.default.writeFileSync(newControl, content);
|
|
1430
|
+
if (oldControl !== newControl && fs_1.default.existsSync(oldControl))
|
|
1431
|
+
fs_1.default.rmSync(oldControl);
|
|
1432
|
+
}
|
|
1433
|
+
changed.push(newControl);
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
warnings.push('missing control file');
|
|
1437
|
+
}
|
|
1438
|
+
const makefilePath = info.Makefile;
|
|
1439
|
+
if (fs_1.default.existsSync(makefilePath)) {
|
|
1440
|
+
if (!dry)
|
|
1441
|
+
(0, writer_1.writeExtensionMakefile)(makefilePath, newName, version);
|
|
1442
|
+
changed.push(makefilePath);
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
warnings.push('missing Makefile');
|
|
1446
|
+
}
|
|
1447
|
+
const oldSql = path_1.default.join(modPath, 'sql', `${info.extname}--${version}.sql`);
|
|
1448
|
+
const newSql = path_1.default.join(modPath, 'sql', `${newName}--${version}.sql`);
|
|
1449
|
+
if (fs_1.default.existsSync(oldSql)) {
|
|
1450
|
+
if (!dry) {
|
|
1451
|
+
if (oldSql !== newSql) {
|
|
1452
|
+
fs_1.default.mkdirSync(path_1.default.dirname(newSql), { recursive: true });
|
|
1453
|
+
fs_1.default.renameSync(oldSql, newSql);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
changed.push(newSql);
|
|
1457
|
+
}
|
|
1458
|
+
else {
|
|
1459
|
+
if (fs_1.default.existsSync(newSql)) {
|
|
1460
|
+
changed.push(newSql);
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
warnings.push('missing combined sql file');
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
this.clearCache();
|
|
1467
|
+
return { changed, warnings };
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
exports.PgpmPackage = PgpmPackage;
|