@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,526 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { sync as glob } from 'glob';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { PgpmPackage } from '../core/class/pgpm';
|
|
5
|
+
import { parsePlanFile } from '../files/plan/parser';
|
|
6
|
+
import { errors } from '@pgpmjs/types';
|
|
7
|
+
/**
|
|
8
|
+
* Core dependency resolution algorithm that handles circular dependency detection
|
|
9
|
+
* and topological sorting of dependencies. This unified implementation eliminates
|
|
10
|
+
* code duplication between getDeps, resolveExtensionDependencies, and resolveDependencies.
|
|
11
|
+
*
|
|
12
|
+
* @param deps - The dependency graph mapping modules to their dependencies
|
|
13
|
+
* @param external - Array to collect external dependencies
|
|
14
|
+
* @param options - Configuration options for customizing resolver behavior
|
|
15
|
+
* @returns A function that performs dependency resolution with the given configuration
|
|
16
|
+
*/
|
|
17
|
+
function createDependencyResolver(deps, external, options) {
|
|
18
|
+
const { handleExternalDep, transformModule, makeKey = (module) => module, extname } = options;
|
|
19
|
+
return function dep_resolve(sqlmodule, resolved, unresolved) {
|
|
20
|
+
unresolved.push(sqlmodule);
|
|
21
|
+
let moduleToResolve = sqlmodule;
|
|
22
|
+
let edges;
|
|
23
|
+
let returnEarly = false;
|
|
24
|
+
if (transformModule) {
|
|
25
|
+
const result = transformModule(sqlmodule, extname);
|
|
26
|
+
moduleToResolve = result.module;
|
|
27
|
+
edges = result.edges;
|
|
28
|
+
returnEarly = result.returnEarly || false;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
edges = deps[makeKey(sqlmodule)];
|
|
32
|
+
}
|
|
33
|
+
// Handle external dependencies if no edges found
|
|
34
|
+
if (!edges) {
|
|
35
|
+
if (handleExternalDep) {
|
|
36
|
+
handleExternalDep(sqlmodule, deps, external);
|
|
37
|
+
edges = deps[sqlmodule] || [];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw errors.MODULE_NOT_FOUND({ name: sqlmodule });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (returnEarly) {
|
|
44
|
+
const index = unresolved.indexOf(sqlmodule);
|
|
45
|
+
unresolved.splice(index, 1);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Process each dependency
|
|
49
|
+
for (const dep of edges) {
|
|
50
|
+
if (!resolved.includes(dep)) {
|
|
51
|
+
if (unresolved.includes(dep)) {
|
|
52
|
+
throw errors.CIRCULAR_DEPENDENCY({ module: moduleToResolve, dependency: dep });
|
|
53
|
+
}
|
|
54
|
+
dep_resolve(dep, resolved, unresolved);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
resolved.push(moduleToResolve);
|
|
58
|
+
const index = unresolved.indexOf(sqlmodule);
|
|
59
|
+
unresolved.splice(index, 1);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generates a standardized key for SQL deployment files
|
|
64
|
+
* @param sqlmodule - The module name (e.g., 'users/create')
|
|
65
|
+
* @returns The standardized file path key (e.g., '/deploy/users/create.sql')
|
|
66
|
+
*/
|
|
67
|
+
const makeKey = (sqlmodule) => `/deploy/${sqlmodule}.sql`;
|
|
68
|
+
/**
|
|
69
|
+
* Resolves dependencies for extension modules using a pre-built module map.
|
|
70
|
+
* This is a simpler version that works with module metadata rather than parsing SQL files.
|
|
71
|
+
*
|
|
72
|
+
* @param name - The name of the module to resolve dependencies for
|
|
73
|
+
* @param modules - Record mapping module names to their dependency requirements
|
|
74
|
+
* @returns Object containing external dependencies and resolved dependency order
|
|
75
|
+
*/
|
|
76
|
+
export const resolveExtensionDependencies = (name, modules) => {
|
|
77
|
+
if (!modules[name]) {
|
|
78
|
+
throw errors.MODULE_NOT_FOUND({ name });
|
|
79
|
+
}
|
|
80
|
+
const external = [];
|
|
81
|
+
const deps = Object.keys(modules).reduce((memo, key) => {
|
|
82
|
+
memo[key] = modules[key].requires;
|
|
83
|
+
return memo;
|
|
84
|
+
}, {});
|
|
85
|
+
// Handle external dependencies for resolveExtensionDependencies - simpler than getDeps
|
|
86
|
+
const handleExternalDep = (dep, deps, external) => {
|
|
87
|
+
external.push(dep);
|
|
88
|
+
deps[dep] = [];
|
|
89
|
+
};
|
|
90
|
+
// Create the dependency resolver with resolveExtensionDependencies-specific configuration
|
|
91
|
+
const dep_resolve = createDependencyResolver(deps, external, {
|
|
92
|
+
handleExternalDep,
|
|
93
|
+
extname: name // For resolveExtensionDependencies, we use the module name as extname
|
|
94
|
+
});
|
|
95
|
+
const resolved = [];
|
|
96
|
+
const unresolved = [];
|
|
97
|
+
dep_resolve(name, resolved, unresolved);
|
|
98
|
+
return { external, resolved };
|
|
99
|
+
};
|
|
100
|
+
//
|
|
101
|
+
//
|
|
102
|
+
//
|
|
103
|
+
//
|
|
104
|
+
//
|
|
105
|
+
// - for each change in the plan, create a node in the dependency graph and add edges for any declared dependencies.
|
|
106
|
+
//
|
|
107
|
+
//
|
|
108
|
+
// - Cross-package references:
|
|
109
|
+
//
|
|
110
|
+
//
|
|
111
|
+
//
|
|
112
|
+
//
|
|
113
|
+
// resolveDependencies overview
|
|
114
|
+
// - Purpose: compute dependency graph and apply order for a package/module.
|
|
115
|
+
// - Sources: 'sql' (parse headers + topo + extensions-first) vs 'plan' (use plan.changes order directly).
|
|
116
|
+
// - Tags: 'preserve' (keep), 'internal' (map for traversal), 'resolve' (replace with change names).
|
|
117
|
+
// - Output: { external, resolved, deps, resolvedTags? }.
|
|
118
|
+
// Detailed notes are placed inline near the relevant code paths below.
|
|
119
|
+
export const resolveDependencies = (packageDir, extname, options = {}) => {
|
|
120
|
+
const { tagResolution = 'preserve', loadPlanFiles = true, planFileLoader, source = 'sql' } = options;
|
|
121
|
+
// For 'resolve' and 'internal' modes, we need plan file loading
|
|
122
|
+
const planCache = {};
|
|
123
|
+
// Helper function to load a plan file for a package
|
|
124
|
+
const loadPlanFile = (packageName) => {
|
|
125
|
+
if (!loadPlanFiles) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (planFileLoader) {
|
|
129
|
+
return planFileLoader(packageName, extname, packageDir);
|
|
130
|
+
}
|
|
131
|
+
if (planCache[packageName]) {
|
|
132
|
+
return planCache[packageName];
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
let planPath;
|
|
136
|
+
if (packageName === extname) {
|
|
137
|
+
// For the current package
|
|
138
|
+
planPath = join(packageDir, 'pgpm.plan');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// For external packages, use PgpmPackage to find module path
|
|
142
|
+
const project = new PgpmPackage(packageDir);
|
|
143
|
+
const moduleMap = project.getModuleMap();
|
|
144
|
+
const module = moduleMap[packageName];
|
|
145
|
+
if (!module) {
|
|
146
|
+
throw errors.MODULE_NOT_FOUND({ name: packageName });
|
|
147
|
+
}
|
|
148
|
+
const workspacePath = project.getWorkspacePath();
|
|
149
|
+
if (!workspacePath) {
|
|
150
|
+
throw new Error(`No workspace found for module ${packageName}`);
|
|
151
|
+
}
|
|
152
|
+
planPath = join(workspacePath, module.path, 'pgpm.plan');
|
|
153
|
+
}
|
|
154
|
+
const result = parsePlanFile(planPath);
|
|
155
|
+
if (result.data) {
|
|
156
|
+
planCache[packageName] = result.data;
|
|
157
|
+
return result.data;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// Plan file not found or parse error
|
|
162
|
+
console.warn(`Could not load plan file for package ${packageName}: ${error}`);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
// Plan-mode branch: use plan.changes order directly; build graph from plan deps (no topo or resort).
|
|
167
|
+
// - Loads the current package plan and throws if missing.
|
|
168
|
+
// - For each change in plan, adds a node; edges come from change.dependencies.
|
|
169
|
+
// - Tag handling per tagResolution: 'preserve' keeps tokens, 'internal' maps for traversal, 'resolve' replaces with change names.
|
|
170
|
+
// - Cross-package refs "pkg:change" are recorded in external and kept as graph nodes for coordination by callers.
|
|
171
|
+
// - Internal refs like "extname:change" are normalized to "change".
|
|
172
|
+
const resolveTagToChange = (projectName, tagName) => {
|
|
173
|
+
const plan = loadPlanFile(projectName);
|
|
174
|
+
if (!plan)
|
|
175
|
+
return null;
|
|
176
|
+
const tag = plan.tags.find(t => t.name === tagName);
|
|
177
|
+
if (!tag)
|
|
178
|
+
return null;
|
|
179
|
+
return tag.change;
|
|
180
|
+
};
|
|
181
|
+
if (source === 'plan') {
|
|
182
|
+
const plan = loadPlanFile(extname);
|
|
183
|
+
if (!plan) {
|
|
184
|
+
throw errors.PLAN_PARSE_ERROR({ planPath: `${extname}/pgpm.plan`, errors: 'Plan file not found or failed to parse while using plan-only resolution' });
|
|
185
|
+
}
|
|
186
|
+
const external = [];
|
|
187
|
+
const deps = {};
|
|
188
|
+
const tagMappings = {};
|
|
189
|
+
const normalizeInternal = (dep) => {
|
|
190
|
+
if (/:/.test(dep)) {
|
|
191
|
+
const [project, localKey] = dep.split(':', 2);
|
|
192
|
+
if (project === extname)
|
|
193
|
+
return localKey;
|
|
194
|
+
}
|
|
195
|
+
return dep;
|
|
196
|
+
};
|
|
197
|
+
const resolveTagDep = (projectName, tagName) => {
|
|
198
|
+
const change = resolveTagToChange(projectName, tagName);
|
|
199
|
+
if (!change)
|
|
200
|
+
return null;
|
|
201
|
+
return `${projectName}:${change}`;
|
|
202
|
+
};
|
|
203
|
+
for (const ch of plan.changes) {
|
|
204
|
+
const key = makeKey(ch.name);
|
|
205
|
+
deps[key] = [];
|
|
206
|
+
const changeDeps = ch.dependencies || [];
|
|
207
|
+
for (const rawDep of changeDeps) {
|
|
208
|
+
let dep = rawDep.trim();
|
|
209
|
+
if (dep.includes('@')) {
|
|
210
|
+
const m = dep.match(/^([^:]+):@(.+)$/);
|
|
211
|
+
if (m) {
|
|
212
|
+
const projectName = m[1];
|
|
213
|
+
const tagName = m[2];
|
|
214
|
+
const resolved = resolveTagDep(projectName, tagName);
|
|
215
|
+
if (resolved) {
|
|
216
|
+
if (tagResolution === 'resolve')
|
|
217
|
+
dep = resolved;
|
|
218
|
+
else if (tagResolution === 'internal')
|
|
219
|
+
tagMappings[dep] = resolved;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const m2 = dep.match(/^@(.+)$/);
|
|
224
|
+
if (m2) {
|
|
225
|
+
const tagName = m2[1];
|
|
226
|
+
const resolved = resolveTagDep(extname, tagName);
|
|
227
|
+
if (resolved) {
|
|
228
|
+
if (tagResolution === 'resolve')
|
|
229
|
+
dep = resolved;
|
|
230
|
+
else if (tagResolution === 'internal')
|
|
231
|
+
tagMappings[dep] = resolved;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (/:/.test(dep)) {
|
|
237
|
+
const [project] = dep.split(':', 2);
|
|
238
|
+
if (project !== extname) {
|
|
239
|
+
external.push(dep);
|
|
240
|
+
if (!deps[dep])
|
|
241
|
+
deps[dep] = [];
|
|
242
|
+
deps[key].push(dep);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
deps[key].push(normalizeInternal(dep));
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
deps[key].push(dep);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const transformModule = (sqlmodule, extnameLocal) => {
|
|
252
|
+
const originalModule = sqlmodule;
|
|
253
|
+
if (tagResolution === 'preserve') {
|
|
254
|
+
let moduleToResolve = sqlmodule;
|
|
255
|
+
let edges = deps[makeKey(sqlmodule)];
|
|
256
|
+
if (/:/.test(sqlmodule)) {
|
|
257
|
+
const [project, localKey] = sqlmodule.split(':', 2);
|
|
258
|
+
if (project === extnameLocal) {
|
|
259
|
+
moduleToResolve = localKey;
|
|
260
|
+
edges = deps[makeKey(localKey)];
|
|
261
|
+
if (!edges)
|
|
262
|
+
throw errors.MODULE_NOT_FOUND({ name: `${localKey} (from ${project}:${localKey})` });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
external.push(sqlmodule);
|
|
266
|
+
deps[sqlmodule] = deps[sqlmodule] || [];
|
|
267
|
+
return { module: sqlmodule, edges: [], returnEarly: true };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
if (!edges)
|
|
272
|
+
throw errors.MODULE_NOT_FOUND({ name: sqlmodule });
|
|
273
|
+
}
|
|
274
|
+
return { module: moduleToResolve, edges };
|
|
275
|
+
}
|
|
276
|
+
if (/:/.test(originalModule)) {
|
|
277
|
+
const [project] = originalModule.split(':', 2);
|
|
278
|
+
if (project !== extnameLocal) {
|
|
279
|
+
external.push(originalModule);
|
|
280
|
+
deps[originalModule] = deps[originalModule] || [];
|
|
281
|
+
return { module: originalModule, edges: [], returnEarly: true };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let moduleToResolve = sqlmodule;
|
|
285
|
+
if (tagResolution === 'internal' && tagMappings[sqlmodule]) {
|
|
286
|
+
moduleToResolve = tagMappings[sqlmodule];
|
|
287
|
+
}
|
|
288
|
+
let edges = deps[makeKey(moduleToResolve)];
|
|
289
|
+
if (/:/.test(moduleToResolve)) {
|
|
290
|
+
const [project, localKey] = moduleToResolve.split(':', 2);
|
|
291
|
+
if (project === extnameLocal) {
|
|
292
|
+
moduleToResolve = localKey;
|
|
293
|
+
edges = deps[makeKey(localKey)];
|
|
294
|
+
if (!edges)
|
|
295
|
+
throw errors.MODULE_NOT_FOUND({ name: `${localKey} (from ${project}:${localKey})` });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
if (!edges) {
|
|
300
|
+
edges = deps[makeKey(sqlmodule)];
|
|
301
|
+
if (!edges)
|
|
302
|
+
throw errors.MODULE_NOT_FOUND({ name: sqlmodule });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (tagResolution === 'internal' && edges) {
|
|
306
|
+
const processedEdges = edges.map(dep => {
|
|
307
|
+
if (/:/.test(dep)) {
|
|
308
|
+
const [project] = dep.split(':', 2);
|
|
309
|
+
if (project !== extnameLocal)
|
|
310
|
+
return dep;
|
|
311
|
+
}
|
|
312
|
+
if (tagMappings[dep])
|
|
313
|
+
return tagMappings[dep];
|
|
314
|
+
return dep;
|
|
315
|
+
});
|
|
316
|
+
return { module: moduleToResolve, edges: processedEdges };
|
|
317
|
+
}
|
|
318
|
+
return { module: moduleToResolve, edges };
|
|
319
|
+
};
|
|
320
|
+
// or extension-first resorting. Externals are still tracked in the deps graph and external array.
|
|
321
|
+
const resolved = plan.changes.map(ch => ch.name);
|
|
322
|
+
return { external, resolved, deps, resolvedTags: tagMappings };
|
|
323
|
+
}
|
|
324
|
+
const external = [];
|
|
325
|
+
const deps = {};
|
|
326
|
+
const tagMappings = {};
|
|
327
|
+
// Process SQL files and build dependency graph
|
|
328
|
+
const files = glob(`${packageDir}/deploy/**/*.sql`);
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
const data = readFileSync(file, 'utf-8');
|
|
331
|
+
const lines = data.split('\n');
|
|
332
|
+
const key = '/' + relative(packageDir, file);
|
|
333
|
+
deps[key] = [];
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
// Handle requires statements
|
|
336
|
+
const requiresMatch = line.match(/^-- requires: (.*)/);
|
|
337
|
+
if (requiresMatch) {
|
|
338
|
+
const dep = requiresMatch[1].trim();
|
|
339
|
+
// For 'preserve' mode, just add the dependency as-is (like original getDeps)
|
|
340
|
+
if (tagResolution === 'preserve') {
|
|
341
|
+
deps[key].push(dep);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// For other modes, handle tag resolution
|
|
345
|
+
if (dep.includes('@')) {
|
|
346
|
+
const match = dep.match(/^([^:]+):@(.+)$/);
|
|
347
|
+
if (match) {
|
|
348
|
+
const [, projectName, tagName] = match;
|
|
349
|
+
const taggedChange = resolveTagToChange(projectName, tagName);
|
|
350
|
+
if (taggedChange) {
|
|
351
|
+
if (tagResolution === 'resolve') {
|
|
352
|
+
// Full resolution: replace tag with actual change
|
|
353
|
+
const resolvedDep = `${projectName}:${taggedChange}`;
|
|
354
|
+
deps[key].push(resolvedDep);
|
|
355
|
+
}
|
|
356
|
+
else if (tagResolution === 'internal') {
|
|
357
|
+
// Internal resolution: keep tag in deps but track mapping
|
|
358
|
+
tagMappings[dep] = `${projectName}:${taggedChange}`;
|
|
359
|
+
deps[key].push(dep);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
// Could not resolve tag, keep it as is
|
|
364
|
+
deps[key].push(dep);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// Invalid tag format, keep as is
|
|
369
|
+
deps[key].push(dep);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// Not a tag, keep as is
|
|
374
|
+
deps[key].push(dep);
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
// Handle deploy statements - exactly as in original
|
|
379
|
+
let m2;
|
|
380
|
+
let keyToTest;
|
|
381
|
+
if (/:/.test(line)) {
|
|
382
|
+
m2 = line.match(/^-- Deploy ([^:]*):([\w\/]+)(?:\s+to\s+pg)?/);
|
|
383
|
+
if (m2) {
|
|
384
|
+
const actualProject = m2[1];
|
|
385
|
+
keyToTest = m2[2];
|
|
386
|
+
if (extname !== actualProject) {
|
|
387
|
+
throw new Error(`Mismatched project name in deploy file:
|
|
388
|
+
Expected project: ${extname}
|
|
389
|
+
Found in line : ${actualProject}
|
|
390
|
+
Line : ${line}`);
|
|
391
|
+
}
|
|
392
|
+
const expectedKey = makeKey(keyToTest);
|
|
393
|
+
if (key !== expectedKey) {
|
|
394
|
+
throw new Error(`Deployment script path or internal name mismatch:
|
|
395
|
+
Expected key : ${key}
|
|
396
|
+
Found in line : ${expectedKey}
|
|
397
|
+
Line : ${line}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
m2 = line.match(/^-- Deploy (.*?)(?:\s+to\s+pg)?\s*$/);
|
|
403
|
+
if (m2) {
|
|
404
|
+
keyToTest = m2[1].trim();
|
|
405
|
+
if (key !== makeKey(keyToTest)) {
|
|
406
|
+
throw new Error('deployment script in wrong place or is named wrong internally\n' + line);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const transformModule = (sqlmodule, extname) => {
|
|
413
|
+
const originalModule = sqlmodule;
|
|
414
|
+
// For 'preserve' mode, use simpler logic (like original getDeps)
|
|
415
|
+
if (tagResolution === 'preserve') {
|
|
416
|
+
let moduleToResolve = sqlmodule;
|
|
417
|
+
let edges = deps[makeKey(sqlmodule)];
|
|
418
|
+
if (/:/.test(sqlmodule)) {
|
|
419
|
+
// Has a prefix — could be internal or external
|
|
420
|
+
const [project, localKey] = sqlmodule.split(':', 2);
|
|
421
|
+
if (project === extname) {
|
|
422
|
+
// Internal reference to current package
|
|
423
|
+
moduleToResolve = localKey;
|
|
424
|
+
edges = deps[makeKey(localKey)];
|
|
425
|
+
if (!edges) {
|
|
426
|
+
throw errors.MODULE_NOT_FOUND({ name: `${localKey} (from ${project}:${localKey})` });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// External reference — always OK, even if not in deps yet
|
|
431
|
+
external.push(sqlmodule);
|
|
432
|
+
deps[sqlmodule] = [];
|
|
433
|
+
return { module: sqlmodule, edges: [], returnEarly: true };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// No prefix — must be internal
|
|
438
|
+
if (!edges) {
|
|
439
|
+
throw errors.MODULE_NOT_FOUND({ name: sqlmodule });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { module: moduleToResolve, edges };
|
|
443
|
+
}
|
|
444
|
+
// Check if the ORIGINAL module (before tag resolution) is external
|
|
445
|
+
if (/:/.test(originalModule)) {
|
|
446
|
+
const [project, localKey] = originalModule.split(':', 2);
|
|
447
|
+
if (project !== extname) {
|
|
448
|
+
// External reference — always OK, even if not in deps yet
|
|
449
|
+
external.push(originalModule);
|
|
450
|
+
deps[originalModule] = deps[originalModule] || [];
|
|
451
|
+
return { module: originalModule, edges: [], returnEarly: true };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// For internal resolution mode, check if this module is a tag and resolve it
|
|
455
|
+
let moduleToResolve = sqlmodule;
|
|
456
|
+
if (tagResolution === 'internal' && tagMappings[sqlmodule]) {
|
|
457
|
+
moduleToResolve = tagMappings[sqlmodule];
|
|
458
|
+
}
|
|
459
|
+
let edges = deps[makeKey(moduleToResolve)];
|
|
460
|
+
if (/:/.test(moduleToResolve)) {
|
|
461
|
+
// Has a prefix — must be internal since we already handled external above
|
|
462
|
+
const [project, localKey] = moduleToResolve.split(':', 2);
|
|
463
|
+
if (project === extname) {
|
|
464
|
+
// Internal reference to current package
|
|
465
|
+
moduleToResolve = localKey;
|
|
466
|
+
edges = deps[makeKey(localKey)];
|
|
467
|
+
if (!edges) {
|
|
468
|
+
throw errors.MODULE_NOT_FOUND({ name: `${localKey} (from ${project}:${localKey})` });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// No prefix — must be internal
|
|
474
|
+
if (!edges) {
|
|
475
|
+
// Check if we have edges for the original module
|
|
476
|
+
edges = deps[makeKey(sqlmodule)];
|
|
477
|
+
if (!edges) {
|
|
478
|
+
throw errors.MODULE_NOT_FOUND({ name: sqlmodule });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// For internal resolution, process dependencies through tag mappings
|
|
483
|
+
if (tagResolution === 'internal' && edges) {
|
|
484
|
+
const processedEdges = edges.map(dep => {
|
|
485
|
+
// Check if this dependency is external - if so, don't resolve tags
|
|
486
|
+
if (/:/.test(dep)) {
|
|
487
|
+
const [project, localKey] = dep.split(':', 2);
|
|
488
|
+
if (project !== extname) {
|
|
489
|
+
// External dependency - keep original tag name
|
|
490
|
+
return dep;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Internal dependency - apply tag mapping if available
|
|
494
|
+
if (tagMappings[dep]) {
|
|
495
|
+
return tagMappings[dep];
|
|
496
|
+
}
|
|
497
|
+
return dep;
|
|
498
|
+
});
|
|
499
|
+
return { module: moduleToResolve, edges: processedEdges };
|
|
500
|
+
}
|
|
501
|
+
return { module: moduleToResolve, edges };
|
|
502
|
+
};
|
|
503
|
+
// Create the dependency resolver with resolveDependencies-specific configuration
|
|
504
|
+
const dep_resolve = createDependencyResolver(deps, external, {
|
|
505
|
+
transformModule,
|
|
506
|
+
makeKey,
|
|
507
|
+
extname
|
|
508
|
+
});
|
|
509
|
+
let resolved = [];
|
|
510
|
+
const unresolved = [];
|
|
511
|
+
// Synthetic root '_virtual/app' seeds local deploy/* modules into resolver for topo ordering.
|
|
512
|
+
// Removed after resolution; not present in returned output.
|
|
513
|
+
// Followed by extension-first reordering for deterministic application in SQL mode only.
|
|
514
|
+
// Add synthetic root node - exactly as in original
|
|
515
|
+
deps[makeKey('_virtual/app')] = Object.keys(deps)
|
|
516
|
+
.filter((dep) => dep.startsWith('/deploy/'))
|
|
517
|
+
.map((dep) => dep.replace(/^\/deploy\//, '').replace(/\.sql$/, ''));
|
|
518
|
+
dep_resolve('_virtual/app', resolved, unresolved);
|
|
519
|
+
const index = resolved.indexOf('_virtual/app');
|
|
520
|
+
resolved.splice(index, 1);
|
|
521
|
+
delete deps[makeKey('_virtual/app')];
|
|
522
|
+
const extensions = resolved.filter((module) => module.startsWith('extensions/'));
|
|
523
|
+
const normalSql = resolved.filter((module) => !module.startsWith('extensions/'));
|
|
524
|
+
resolved = [...extensions, ...normalSql];
|
|
525
|
+
return { external, resolved, deps, resolvedTags: tagMappings };
|
|
526
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { getChanges, getExtensionName } from '../files';
|
|
3
|
+
import { parsePlanFile } from '../files/plan/parser';
|
|
4
|
+
import { resolveDependencies } from './deps';
|
|
5
|
+
import { errors } from '@pgpmjs/types';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves SQL scripts for deployment or reversion.
|
|
8
|
+
*
|
|
9
|
+
* @param pkgDir - The package directory (defaults to the current working directory).
|
|
10
|
+
* @param scriptType - The type of script to resolve (`deploy` or `revert`).
|
|
11
|
+
* @returns A single concatenated SQL script as a string.
|
|
12
|
+
*/
|
|
13
|
+
export const resolve = (pkgDir = process.cwd(), scriptType = 'deploy') => {
|
|
14
|
+
const sqlfile = [];
|
|
15
|
+
const name = getExtensionName(pkgDir);
|
|
16
|
+
const { resolved, external } = resolveDependencies(pkgDir, name, { tagResolution: 'resolve' });
|
|
17
|
+
const scripts = scriptType === 'revert' ? [...resolved].reverse() : resolved;
|
|
18
|
+
for (const script of scripts) {
|
|
19
|
+
if (external.includes(script))
|
|
20
|
+
continue;
|
|
21
|
+
const file = `${pkgDir}/${scriptType}/${script}.sql`;
|
|
22
|
+
const dscript = readFileSync(file, 'utf-8');
|
|
23
|
+
sqlfile.push(dscript);
|
|
24
|
+
}
|
|
25
|
+
return sqlfile.join('\n');
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Resolves SQL scripts based on the `pgpm.plan` file.
|
|
29
|
+
*
|
|
30
|
+
* @param pkgDir - The package directory (defaults to the current working directory).
|
|
31
|
+
* @param scriptType - The type of script to resolve (`deploy` or `revert`).
|
|
32
|
+
* @returns A single concatenated SQL script as a string.
|
|
33
|
+
*/
|
|
34
|
+
export const resolveWithPlan = (pkgDir = process.cwd(), scriptType = 'deploy') => {
|
|
35
|
+
const sqlfile = [];
|
|
36
|
+
const planPath = `${pkgDir}/pgpm.plan`;
|
|
37
|
+
let resolved = getChanges(planPath);
|
|
38
|
+
if (scriptType === 'revert') {
|
|
39
|
+
resolved = resolved.reverse();
|
|
40
|
+
}
|
|
41
|
+
for (const script of resolved) {
|
|
42
|
+
const file = `${pkgDir}/${scriptType}/${script}.sql`;
|
|
43
|
+
const dscript = readFileSync(file, 'utf-8');
|
|
44
|
+
sqlfile.push(dscript);
|
|
45
|
+
}
|
|
46
|
+
return sqlfile.join('\n');
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Resolves a tag reference to its corresponding change name.
|
|
50
|
+
* Tags provide a way to reference specific points in a package's deployment history.
|
|
51
|
+
*
|
|
52
|
+
* @param planPath - Path to the plan file containing tag definitions
|
|
53
|
+
* @param tagReference - The tag reference to resolve (e.g., "package:@tagName" or "@tagName")
|
|
54
|
+
* @param currentPackage - The current package name (used when tag doesn't specify package)
|
|
55
|
+
* @returns The resolved change name
|
|
56
|
+
* @throws Error if tag format is invalid or tag is not found
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // Resolve a tag in the current package
|
|
60
|
+
* resolveTagToChangeName('/path/to/pgpm.plan', '@v1.0.0', 'mypackage')
|
|
61
|
+
* // Returns: 'schema/v1'
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Resolve a tag from another package
|
|
65
|
+
* resolveTagToChangeName('/path/to/pgpm.plan', 'auth:@v2.0.0')
|
|
66
|
+
* // Returns: 'users/table'
|
|
67
|
+
*/
|
|
68
|
+
export const resolveTagToChangeName = (planPath, tagReference, currentProject) => {
|
|
69
|
+
// If not a tag reference, return as-is
|
|
70
|
+
if (!tagReference.includes('@')) {
|
|
71
|
+
return tagReference;
|
|
72
|
+
}
|
|
73
|
+
// Handle simple tag format (@tagName) by prepending current package
|
|
74
|
+
if (tagReference.startsWith('@') && !tagReference.includes(':')) {
|
|
75
|
+
if (!currentProject) {
|
|
76
|
+
const plan = parsePlanFile(planPath);
|
|
77
|
+
if (!plan.data) {
|
|
78
|
+
throw errors.PLAN_PARSE_ERROR({ planPath, errors: 'Could not parse plan file' });
|
|
79
|
+
}
|
|
80
|
+
currentProject = plan.data.package;
|
|
81
|
+
}
|
|
82
|
+
tagReference = `${currentProject}:${tagReference}`;
|
|
83
|
+
}
|
|
84
|
+
// Parse package:@tagName format
|
|
85
|
+
const match = tagReference.match(/^([^:]+):@(.+)$/);
|
|
86
|
+
if (!match) {
|
|
87
|
+
throw errors.INVALID_NAME({ name: tagReference, type: 'tag', rules: 'Expected format: package:@tagName or @tagName' });
|
|
88
|
+
}
|
|
89
|
+
const [, projectName, tagName] = match;
|
|
90
|
+
// Parse plan file to find tag
|
|
91
|
+
const planResult = parsePlanFile(planPath);
|
|
92
|
+
if (!planResult.data) {
|
|
93
|
+
throw errors.PLAN_PARSE_ERROR({ planPath, errors: 'Could not parse plan file' });
|
|
94
|
+
}
|
|
95
|
+
// Find the tag in the plan
|
|
96
|
+
const tag = planResult.data.tags?.find((t) => t.name === tagName);
|
|
97
|
+
if (!tag) {
|
|
98
|
+
throw errors.TAG_NOT_FOUND({ tag: tagName, project: projectName });
|
|
99
|
+
}
|
|
100
|
+
return tag.change;
|
|
101
|
+
};
|