@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.
Files changed (140) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +99 -0
  3. package/core/boilerplate-scanner.d.ts +41 -0
  4. package/core/boilerplate-scanner.js +106 -0
  5. package/core/boilerplate-types.d.ts +52 -0
  6. package/core/boilerplate-types.js +6 -0
  7. package/core/class/pgpm.d.ts +150 -0
  8. package/core/class/pgpm.js +1470 -0
  9. package/core/template-scaffold.d.ts +29 -0
  10. package/core/template-scaffold.js +168 -0
  11. package/esm/core/boilerplate-scanner.js +96 -0
  12. package/esm/core/boilerplate-types.js +5 -0
  13. package/esm/core/class/pgpm.js +1430 -0
  14. package/esm/core/template-scaffold.js +161 -0
  15. package/esm/export/export-meta.js +240 -0
  16. package/esm/export/export-migrations.js +180 -0
  17. package/esm/extensions/extensions.js +31 -0
  18. package/esm/files/extension/index.js +3 -0
  19. package/esm/files/extension/reader.js +79 -0
  20. package/esm/files/extension/writer.js +63 -0
  21. package/esm/files/index.js +6 -0
  22. package/esm/files/plan/generator.js +49 -0
  23. package/esm/files/plan/index.js +5 -0
  24. package/esm/files/plan/parser.js +296 -0
  25. package/esm/files/plan/validators.js +181 -0
  26. package/esm/files/plan/writer.js +114 -0
  27. package/esm/files/sql/index.js +1 -0
  28. package/esm/files/sql/writer.js +107 -0
  29. package/esm/files/sql-scripts/index.js +2 -0
  30. package/esm/files/sql-scripts/reader.js +19 -0
  31. package/esm/files/types/index.js +1 -0
  32. package/esm/files/types/package.js +1 -0
  33. package/esm/index.js +21 -0
  34. package/esm/init/client.js +144 -0
  35. package/esm/init/sql/bootstrap-roles.sql +55 -0
  36. package/esm/init/sql/bootstrap-test-roles.sql +72 -0
  37. package/esm/migrate/clean.js +23 -0
  38. package/esm/migrate/client.js +551 -0
  39. package/esm/migrate/index.js +5 -0
  40. package/esm/migrate/sql/procedures.sql +258 -0
  41. package/esm/migrate/sql/schema.sql +37 -0
  42. package/esm/migrate/types.js +1 -0
  43. package/esm/migrate/utils/event-logger.js +28 -0
  44. package/esm/migrate/utils/hash.js +27 -0
  45. package/esm/migrate/utils/transaction.js +125 -0
  46. package/esm/modules/modules.js +49 -0
  47. package/esm/packaging/package.js +96 -0
  48. package/esm/packaging/transform.js +70 -0
  49. package/esm/projects/deploy.js +123 -0
  50. package/esm/projects/revert.js +75 -0
  51. package/esm/projects/verify.js +61 -0
  52. package/esm/resolution/deps.js +526 -0
  53. package/esm/resolution/resolve.js +101 -0
  54. package/esm/utils/debug.js +147 -0
  55. package/esm/utils/target-utils.js +37 -0
  56. package/esm/workspace/paths.js +43 -0
  57. package/esm/workspace/utils.js +31 -0
  58. package/export/export-meta.d.ts +8 -0
  59. package/export/export-meta.js +244 -0
  60. package/export/export-migrations.d.ts +17 -0
  61. package/export/export-migrations.js +187 -0
  62. package/extensions/extensions.d.ts +5 -0
  63. package/extensions/extensions.js +35 -0
  64. package/files/extension/index.d.ts +2 -0
  65. package/files/extension/index.js +19 -0
  66. package/files/extension/reader.d.ts +24 -0
  67. package/files/extension/reader.js +86 -0
  68. package/files/extension/writer.d.ts +39 -0
  69. package/files/extension/writer.js +70 -0
  70. package/files/index.d.ts +5 -0
  71. package/files/index.js +22 -0
  72. package/files/plan/generator.d.ts +22 -0
  73. package/files/plan/generator.js +57 -0
  74. package/files/plan/index.d.ts +4 -0
  75. package/files/plan/index.js +21 -0
  76. package/files/plan/parser.d.ts +27 -0
  77. package/files/plan/parser.js +303 -0
  78. package/files/plan/validators.d.ts +52 -0
  79. package/files/plan/validators.js +187 -0
  80. package/files/plan/writer.d.ts +27 -0
  81. package/files/plan/writer.js +124 -0
  82. package/files/sql/index.d.ts +1 -0
  83. package/files/sql/index.js +17 -0
  84. package/files/sql/writer.d.ts +12 -0
  85. package/files/sql/writer.js +114 -0
  86. package/files/sql-scripts/index.d.ts +1 -0
  87. package/files/sql-scripts/index.js +18 -0
  88. package/files/sql-scripts/reader.d.ts +8 -0
  89. package/files/sql-scripts/reader.js +23 -0
  90. package/files/types/index.d.ts +46 -0
  91. package/files/types/index.js +17 -0
  92. package/files/types/package.d.ts +20 -0
  93. package/files/types/package.js +2 -0
  94. package/index.d.ts +21 -0
  95. package/index.js +45 -0
  96. package/init/client.d.ts +26 -0
  97. package/init/client.js +148 -0
  98. package/init/sql/bootstrap-roles.sql +55 -0
  99. package/init/sql/bootstrap-test-roles.sql +72 -0
  100. package/migrate/clean.d.ts +1 -0
  101. package/migrate/clean.js +27 -0
  102. package/migrate/client.d.ts +80 -0
  103. package/migrate/client.js +555 -0
  104. package/migrate/index.d.ts +5 -0
  105. package/migrate/index.js +21 -0
  106. package/migrate/sql/procedures.sql +258 -0
  107. package/migrate/sql/schema.sql +37 -0
  108. package/migrate/types.d.ts +67 -0
  109. package/migrate/types.js +2 -0
  110. package/migrate/utils/event-logger.d.ts +13 -0
  111. package/migrate/utils/event-logger.js +32 -0
  112. package/migrate/utils/hash.d.ts +12 -0
  113. package/migrate/utils/hash.js +32 -0
  114. package/migrate/utils/transaction.d.ts +27 -0
  115. package/migrate/utils/transaction.js +129 -0
  116. package/modules/modules.d.ts +31 -0
  117. package/modules/modules.js +56 -0
  118. package/package.json +70 -0
  119. package/packaging/package.d.ts +19 -0
  120. package/packaging/package.js +102 -0
  121. package/packaging/transform.d.ts +22 -0
  122. package/packaging/transform.js +75 -0
  123. package/projects/deploy.d.ts +8 -0
  124. package/projects/deploy.js +160 -0
  125. package/projects/revert.d.ts +15 -0
  126. package/projects/revert.js +112 -0
  127. package/projects/verify.d.ts +8 -0
  128. package/projects/verify.js +98 -0
  129. package/resolution/deps.d.ts +57 -0
  130. package/resolution/deps.js +531 -0
  131. package/resolution/resolve.d.ts +37 -0
  132. package/resolution/resolve.js +107 -0
  133. package/utils/debug.d.ts +21 -0
  134. package/utils/debug.js +153 -0
  135. package/utils/target-utils.d.ts +5 -0
  136. package/utils/target-utils.js +40 -0
  137. package/workspace/paths.d.ts +14 -0
  138. package/workspace/paths.js +50 -0
  139. package/workspace/utils.d.ts +8 -0
  140. 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
+ };