@pgpmjs/core 7.11.1 → 7.14.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.
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { basename, dirname, join } from 'path';
3
+ import { APPLY_SPEC_FILE } from './types';
4
+ /** Whether a module directory is an apply-spec proxy module. */
5
+ export function hasApplySpec(moduleDir) {
6
+ return existsSync(join(moduleDir, APPLY_SPEC_FILE));
7
+ }
8
+ /**
9
+ * Read, validate, and normalize a proxy module's `pgpm.apply.json`.
10
+ *
11
+ * Normalizations: `source` is expanded to its object form and `name` defaults
12
+ * to the proxy directory's name.
13
+ *
14
+ * @throws when the file is missing, unparsable, or structurally invalid.
15
+ */
16
+ export function readApplySpec(moduleDir) {
17
+ const specPath = join(moduleDir, APPLY_SPEC_FILE);
18
+ if (!existsSync(specPath)) {
19
+ throw new Error(`No ${APPLY_SPEC_FILE} found in ${moduleDir}`);
20
+ }
21
+ return parseApplySpec(readFileSync(specPath, 'utf-8'), specPath);
22
+ }
23
+ /** In-memory core of {@link readApplySpec}. */
24
+ export function parseApplySpec(content, specPath) {
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(content);
28
+ }
29
+ catch (err) {
30
+ throw new Error(`Failed to parse ${specPath}: ${err instanceof Error ? err.message : err}`);
31
+ }
32
+ const source = typeof parsed?.source === 'string' ? { module: parsed.source } : parsed?.source;
33
+ if (!source || typeof source.module !== 'string' || !source.module) {
34
+ throw new Error(`${specPath}: "source" (source module name, or { "module": ... }) is required`);
35
+ }
36
+ const hasSchemas = parsed.schemas !== undefined;
37
+ if (hasSchemas) {
38
+ if (typeof parsed.schemas !== 'object' ||
39
+ Array.isArray(parsed.schemas) ||
40
+ Object.keys(parsed.schemas).length === 0 ||
41
+ Object.entries(parsed.schemas).some(([from, to]) => typeof from !== 'string' || typeof to !== 'string' || !from || !to)) {
42
+ throw new Error(`${specPath}: "schemas" must be a non-empty string → string map`);
43
+ }
44
+ }
45
+ const ROUTE_KINDS = ['table', 'view', 'function', 'procedure', 'type'];
46
+ const hasRoute = parsed.route !== undefined;
47
+ if (hasRoute) {
48
+ if (!Array.isArray(parsed.route) || parsed.route.length === 0) {
49
+ throw new Error(`${specPath}: "route" must be a non-empty array of route entries`);
50
+ }
51
+ for (const entry of parsed.route) {
52
+ if (!entry ||
53
+ typeof entry !== 'object' ||
54
+ typeof entry.fromSchema !== 'string' ||
55
+ !entry.fromSchema ||
56
+ typeof entry.name !== 'string' ||
57
+ !entry.name ||
58
+ typeof entry.toSchema !== 'string' ||
59
+ !entry.toSchema ||
60
+ !ROUTE_KINDS.includes(entry.kind)) {
61
+ throw new Error(`${specPath}: each "route" entry needs { fromSchema, kind (${ROUTE_KINDS.join('|')}), name, toSchema } as non-empty strings`);
62
+ }
63
+ }
64
+ }
65
+ if (!hasSchemas && !hasRoute) {
66
+ throw new Error(`${specPath}: at least one of "schemas" (string → string map) or "route" (object routes) is required`);
67
+ }
68
+ const name = parsed.name ?? basename(dirname(specPath));
69
+ if (typeof name !== 'string' || !name) {
70
+ throw new Error(`${specPath}: "name" must be a non-empty string`);
71
+ }
72
+ if (name === source.module) {
73
+ throw new Error(`${specPath}: apply module "${name}" cannot apply itself`);
74
+ }
75
+ const requires = parsed.requires;
76
+ if (requires !== undefined &&
77
+ (!Array.isArray(requires) || requires.some((r) => typeof r !== 'string' || !r))) {
78
+ throw new Error(`${specPath}: "requires" must be an array of module names`);
79
+ }
80
+ if (requires?.includes(source.module)) {
81
+ throw new Error(`${specPath}: "requires" must not include the source module "${source.module}" — ` +
82
+ `it is source material, not a runtime dependency`);
83
+ }
84
+ return { ...parsed, name, source, requires };
85
+ }
@@ -0,0 +1,3 @@
1
+ export * from './materialize';
2
+ export * from './apply-spec';
3
+ export * from './types';
@@ -0,0 +1,116 @@
1
+ import { mkdtempSync } from 'fs';
2
+ import { tmpdir } from 'os';
3
+ import { join, resolve } from 'path';
4
+ import { bundleFromModule, materializeBundle, transpileBundle, verifyBundle } from '@pgpmjs/bundle';
5
+ import { loadModule, makeSchemaTranspiler } from '@pgpmjs/transform';
6
+ import { hasApplySpec, readApplySpec } from './apply-spec';
7
+ // Functions that take a bare schema name as a string-literal first argument.
8
+ // The AST pass only remaps dotted literals ('schema.object'); these helpers
9
+ // (ubiquitous in verify scripts) name the schema alone, so a string-level
10
+ // pre-pass remaps them.
11
+ const SCHEMA_NAME_LITERAL_FUNCS = ['verify_schema', 'has_schema_privilege'];
12
+ const schemaNameLiteralPass = (content, schemaMapping) => {
13
+ const pattern = new RegExp(`\\b(${SCHEMA_NAME_LITERAL_FUNCS.join('|')})\\s*\\(\\s*'([^']+)'`, 'gi');
14
+ return content.replace(pattern, (match, _fn, schema) => {
15
+ const mapped = schemaMapping.get(schema);
16
+ return mapped ? match.replace(`'${schema}'`, `'${mapped}'`) : match;
17
+ });
18
+ };
19
+ /**
20
+ * Transpile a source module per an apply spec and materialize the result as a
21
+ * deployable module directory carrying the instance's identity.
22
+ *
23
+ * Pure pipeline over existing primitives: `bundleFromModule` →
24
+ * `transpileBundle(makeSchemaTranspiler)` → `verifyBundle` →
25
+ * `materializeBundle`. Deterministic — identical source + spec always produce
26
+ * an identical bundle (and digest), which is what makes verify/revert able to
27
+ * re-derive the exact deployed artifact.
28
+ */
29
+ export async function materializeApplyModule(options) {
30
+ const { sourceDir, spec } = options;
31
+ const instanceName = spec.name;
32
+ // the schema transpiler's SQL/PLpgSQL parser is WASM-backed and needs a
33
+ // one-time async init (idempotent)
34
+ await loadModule();
35
+ const source = bundleFromModule(sourceDir);
36
+ if (spec.source.bundleDigest && spec.source.bundleDigest !== source.manifest.digest) {
37
+ throw new Error(`Apply spec for "${instanceName}" pins source bundle digest ${spec.source.bundleDigest}, ` +
38
+ `but the installed source "${spec.source.module}" hashes to ${source.manifest.digest}. ` +
39
+ `Reinstall the pinned version or update the spec.`);
40
+ }
41
+ // Every distinct target schema is treated as "may already exist": apply
42
+ // deploys into consumer-chosen namespaces that can pre-exist (a shared schema,
43
+ // another tenant), so `CREATE SCHEMA` is emitted idempotently. This is what
44
+ // lets the same source apply cleanly whether or not the destination exists.
45
+ const targetSchemas = [
46
+ ...new Set([
47
+ ...Object.values(spec.schemas ?? {}),
48
+ ...(spec.route ?? []).map(r => r.toSchema)
49
+ ])
50
+ ];
51
+ const { renameChange, transformScript, result } = makeSchemaTranspiler({
52
+ schemaMap: spec.schemas,
53
+ routes: spec.route,
54
+ transform: {
55
+ prePasses: [schemaNameLiteralPass],
56
+ assumeSchemasExist: targetSchemas
57
+ }
58
+ });
59
+ const transpiled = transpileBundle(source, {
60
+ renameChange,
61
+ transformScript,
62
+ renameModule: instanceName,
63
+ provenance: {
64
+ appliedFrom: spec.source.module,
65
+ ...(spec.source.package ? { sourcePackage: spec.source.package } : {}),
66
+ ...(spec.source.version ? { sourceVersion: spec.source.version } : {})
67
+ }
68
+ });
69
+ if (result.errors.length > 0) {
70
+ const detail = result.errors.map(e => `${e.file}: ${e.error}`).join('; ');
71
+ throw new Error(`Apply transpile of "${spec.source.module}" as "${instanceName}" failed: ${detail}`);
72
+ }
73
+ const issues = verifyBundle(transpiled);
74
+ if (issues.length > 0) {
75
+ throw new Error(`Transpiled bundle for "${instanceName}" failed integrity verification: ` +
76
+ issues.map(i => i.kind).join(', '));
77
+ }
78
+ const outDir = options.outDir ?? mkdtempSync(join(tmpdir(), `pgpm-apply-${instanceName}-`));
79
+ materializeBundle(transpiled, outDir);
80
+ return { bundle: transpiled, outDir };
81
+ }
82
+ // One materialization per proxy path per process: the pipeline is
83
+ // deterministic, so re-deriving within a process is pure waste.
84
+ const materializedCache = new Map();
85
+ /**
86
+ * Deploy-path dispatch seam: given a resolved module's directory, return the
87
+ * directory the migration engine should actually run against.
88
+ *
89
+ * Non-proxy modules pass through unchanged. For proxy modules (directories
90
+ * carrying a `pgpm.apply.json`), the source module is located in the workspace
91
+ * module map, transpiled per the spec, and materialized to a cached temp dir
92
+ * carrying the instance's identity.
93
+ */
94
+ export async function resolveEffectiveModulePath(moduleName, modulePath, moduleMap, workspacePath) {
95
+ if (!hasApplySpec(modulePath))
96
+ return modulePath;
97
+ const cached = materializedCache.get(modulePath);
98
+ if (cached)
99
+ return cached;
100
+ const spec = readApplySpec(modulePath);
101
+ const sourceModule = moduleMap[spec.source.module];
102
+ if (!sourceModule) {
103
+ throw new Error(`Apply spec in "${moduleName}" references source module "${spec.source.module}", ` +
104
+ `which was not found in the workspace. Run \`pgpm install\` to install it` +
105
+ (spec.source.package ? ` (${spec.source.package})` : '') +
106
+ `.`);
107
+ }
108
+ const sourceDir = resolve(workspacePath, sourceModule.path);
109
+ const { outDir } = await materializeApplyModule({ sourceDir, spec });
110
+ materializedCache.set(modulePath, outDir);
111
+ return outDir;
112
+ }
113
+ /** Test seam: clear the per-process materialization cache. */
114
+ export function clearApplyMaterializationCache() {
115
+ materializedCache.clear();
116
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The apply-spec ("proxy module") types.
3
+ *
4
+ * A proxy module is a directory containing a single `pgpm.apply.json` file:
5
+ * "apply <source module> transpiled with this schema map, under my name". It
6
+ * needs no `.control`, no `pgpm.plan`, and no deploy/revert/verify scripts —
7
+ * workspace discovery synthesizes a module entry from the spec, so the proxy
8
+ * participates in dependency resolution by name exactly like a regular
9
+ * module. At deploy/verify/revert time the engine transpiles the source
10
+ * module in memory and runs the normal migration path against the result;
11
+ * transpiled code is never committed.
12
+ */
13
+ /** File name of the apply spec inside a proxy module directory. */
14
+ export const APPLY_SPEC_FILE = 'pgpm.apply.json';
@@ -9,6 +9,9 @@ import { parse } from 'parse-package-name';
9
9
  import path, { dirname, resolve } from 'path';
10
10
  import { getPgPool } from 'pg-cache';
11
11
  import yanse from 'yanse';
12
+ import { resolveEffectiveModulePath } from '../../apply/materialize';
13
+ import { hasApplySpec, readApplySpec } from '../../apply/apply-spec';
14
+ import { APPLY_SPEC_FILE } from '../../apply/types';
12
15
  import { getAvailableExtensions } from '../../extensions/extensions';
13
16
  import { generatePlan, writePlan, writePlanFile } from '@pgpmjs/ast/files';
14
17
  import { getExtensionInfo, getExtensionName, getInstalledExtensions, parseControlFile, writeExtensions } from '@pgpmjs/ast/files';
@@ -242,11 +245,44 @@ export class PgpmPackage {
242
245
  }
243
246
  });
244
247
  // Parse the selected control files
245
- return Array.from(selectedFiles.entries()).reduce((acc, [moduleName, file]) => {
248
+ const moduleMap = Array.from(selectedFiles.entries()).reduce((acc, [moduleName, file]) => {
246
249
  const module = parseControlFile(file, this.workspacePath);
247
250
  acc[moduleName] = module;
248
251
  return acc;
249
252
  }, {});
253
+ return this.addApplyModules(moduleMap, packageDirs);
254
+ }
255
+ /**
256
+ * Discover apply-spec proxy modules (`pgpm.apply.json`) and synthesize
257
+ * module-map entries for them so they participate in dependency resolution
258
+ * by name like any other module. A proxy needs no `.control` or `pgpm.plan`;
259
+ * its `requires` default to the source module's requires. Control-file
260
+ * modules win name collisions.
261
+ */
262
+ addApplyModules(moduleMap, packageDirs) {
263
+ const specFiles = [...new Set(packageDirs.flatMap(dir => glob.sync(globPattern(dir, `**/${APPLY_SPEC_FILE}`))))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
264
+ for (const file of specFiles) {
265
+ let spec;
266
+ try {
267
+ spec = readApplySpec(path.dirname(file));
268
+ }
269
+ catch (err) {
270
+ console.warn(`Skipping invalid apply spec ${file}: ${err instanceof Error ? err.message : err}`);
271
+ continue;
272
+ }
273
+ const name = spec.name;
274
+ if (moduleMap[name])
275
+ continue;
276
+ const source = moduleMap[spec.source.module];
277
+ moduleMap[name] = {
278
+ path: path
279
+ .dirname(path.relative(this.workspacePath, file))
280
+ .replace(/\\/g, '/'),
281
+ requires: spec.requires ?? source?.requires ?? [],
282
+ version: spec.version ?? source?.version ?? '0.0.1'
283
+ };
284
+ }
285
+ return moduleMap;
250
286
  }
251
287
  getModuleMap() {
252
288
  if (!this.workspacePath)
@@ -1196,6 +1232,38 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1196
1232
  external: external
1197
1233
  };
1198
1234
  }
1235
+ /**
1236
+ * Dependency closure for a named module. Proxy (apply-spec) modules have no
1237
+ * pgpm.plan, so they resolve straight off the workspace module map instead
1238
+ * of instantiating a module-rooted package.
1239
+ */
1240
+ getModuleExtensionsFor(name) {
1241
+ const modules = this.getModuleMap();
1242
+ if (!modules[name]) {
1243
+ throw errors.MODULE_NOT_FOUND({ name });
1244
+ }
1245
+ const modulePath = resolve(this.workspacePath, modules[name].path);
1246
+ if (hasApplySpec(modulePath)) {
1247
+ return resolveExtensionDependencies(name, modules);
1248
+ }
1249
+ return this.getModuleProject(name).getModuleExtensions();
1250
+ }
1251
+ /**
1252
+ * Directory the migration engine should run against for a named module:
1253
+ * the module's own directory, or — for proxy (apply-spec) modules — the
1254
+ * materialized transpiled instance.
1255
+ */
1256
+ async getEffectiveModulePathFor(name) {
1257
+ const modules = this.getModuleMap();
1258
+ if (!modules[name]) {
1259
+ throw errors.MODULE_NOT_FOUND({ name });
1260
+ }
1261
+ const modulePath = resolve(this.workspacePath, modules[name].path);
1262
+ if (hasApplySpec(modulePath)) {
1263
+ return resolveEffectiveModulePath(name, modulePath, modules, this.workspacePath);
1264
+ }
1265
+ return this.getModuleProject(name).getModulePath();
1266
+ }
1199
1267
  parsePackageTarget(target) {
1200
1268
  let name;
1201
1269
  let toChange;
@@ -1240,8 +1308,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1240
1308
  extensions = await this.resolveWorkspaceExtensionDependencies();
1241
1309
  }
1242
1310
  else {
1243
- const moduleProject = this.getModuleProject(name);
1244
- extensions = moduleProject.getModuleExtensions();
1311
+ extensions = this.getModuleExtensionsFor(name);
1245
1312
  }
1246
1313
  const pgPool = getPgPool(opts.pg);
1247
1314
  const targetDescription = name === null ? 'all modules' : name;
@@ -1254,10 +1321,9 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1254
1321
  await pgPool.query(msg);
1255
1322
  }
1256
1323
  else {
1257
- const modulePath = resolve(this.workspacePath, modules[extension].path);
1324
+ const modulePath = await resolveEffectiveModulePath(extension, resolve(this.workspacePath, modules[extension].path), modules, this.workspacePath);
1258
1325
  log.info(`📂 Deploying local module: ${extension}`);
1259
1326
  if (opts.deployment.fast) {
1260
- const localProject = this.getModuleProject(extension);
1261
1327
  const cacheKey = getCacheKey(opts.pg, extension, opts.pg.database);
1262
1328
  if (opts.deployment.cache && deployFastCache[cacheKey]) {
1263
1329
  log.warn(`⚡ Using cached pkg for ${extension}.`);
@@ -1266,7 +1332,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1266
1332
  }
1267
1333
  let pkg;
1268
1334
  try {
1269
- pkg = await packageModule(localProject.modulePath, {
1335
+ pkg = await packageModule(modulePath, {
1270
1336
  usePlan: opts.deployment.usePlan,
1271
1337
  extension: false
1272
1338
  });
@@ -1335,8 +1401,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1335
1401
  if (name === null) {
1336
1402
  throw errors.WORKSPACE_OPERATION_ERROR({ operation: 'deployment' });
1337
1403
  }
1338
- const moduleProject = this.getModuleProject(name);
1339
- const modulePath = moduleProject.getModulePath();
1404
+ const modulePath = await this.getEffectiveModulePathFor(name);
1340
1405
  if (!modulePath) {
1341
1406
  throw errors.PATH_NOT_FOUND({ path: name, type: 'module' });
1342
1407
  }
@@ -1403,7 +1468,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1403
1468
  }
1404
1469
  }
1405
1470
  else {
1406
- const modulePath = resolve(this.workspacePath, modules[extension].path);
1471
+ const modulePath = await resolveEffectiveModulePath(extension, resolve(this.workspacePath, modules[extension].path), modules, this.workspacePath);
1407
1472
  log.info(`📂 Reverting local module: ${extension}`);
1408
1473
  try {
1409
1474
  const client = new PgpmMigrate(opts.pg);
@@ -1436,8 +1501,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1436
1501
  if (name === null) {
1437
1502
  throw errors.WORKSPACE_OPERATION_ERROR({ operation: 'revert' });
1438
1503
  }
1439
- const moduleProject = this.getModuleProject(name);
1440
- const modulePath = moduleProject.getModulePath();
1504
+ const modulePath = await this.getEffectiveModulePathFor(name);
1441
1505
  if (!modulePath) {
1442
1506
  throw errors.PATH_NOT_FOUND({ path: name, type: 'module' });
1443
1507
  }
@@ -1464,8 +1528,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1464
1528
  extensions = await this.resolveWorkspaceExtensionDependencies();
1465
1529
  }
1466
1530
  else {
1467
- const moduleProject = this.getModuleProject(name);
1468
- extensions = moduleProject.getModuleExtensions();
1531
+ extensions = this.getModuleExtensionsFor(name);
1469
1532
  }
1470
1533
  const pgPool = getPgPool(opts.pg);
1471
1534
  const targetDescription = name === null ? 'all modules' : name;
@@ -1478,7 +1541,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1478
1541
  await pgPool.query(query, [extension]);
1479
1542
  }
1480
1543
  else {
1481
- const modulePath = resolve(this.workspacePath, modules[extension].path);
1544
+ const modulePath = await resolveEffectiveModulePath(extension, resolve(this.workspacePath, modules[extension].path), modules, this.workspacePath);
1482
1545
  log.info(`📂 Verifying local module: ${extension}`);
1483
1546
  try {
1484
1547
  const client = new PgpmMigrate(opts.pg);
@@ -1510,8 +1573,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1510
1573
  if (name === null) {
1511
1574
  throw errors.WORKSPACE_OPERATION_ERROR({ operation: 'verification' });
1512
1575
  }
1513
- const moduleProject = this.getModuleProject(name);
1514
- const modulePath = moduleProject.getModulePath();
1576
+ const modulePath = await this.getEffectiveModulePathFor(name);
1515
1577
  if (!modulePath) {
1516
1578
  throw errors.PATH_NOT_FOUND({ path: name, type: 'module' });
1517
1579
  }
package/esm/index.js CHANGED
@@ -16,6 +16,7 @@ export * from './core/boilerplate-scanner';
16
16
  // Re-export the module AST layer (moved to the @pgpmjs/ast leaf package)
17
17
  export * from '@pgpmjs/ast/files';
18
18
  export * from '@pgpmjs/ast/module';
19
+ export * from './apply';
19
20
  export * from './bundle';
20
21
  export * from './refactor';
21
22
  export { cleanSql } from './migrate/clean';
@@ -4,9 +4,12 @@ import { errors } from '@pgpmjs/types';
4
4
  import { resolve } from 'path';
5
5
  import * as path from 'path';
6
6
  import { getPgPool } from 'pg-cache';
7
+ import { resolveEffectiveModulePath } from '../apply/materialize';
8
+ import { hasApplySpec } from '../apply/apply-spec';
7
9
  import { PgpmPackage } from '../core/class/pgpm';
8
10
  import { PgpmMigrate } from '../migrate/client';
9
11
  import { packageModule } from '../packaging/package';
12
+ import { resolveExtensionDependencies } from '../resolution/deps';
10
13
  // Cache for fast deployment
11
14
  const deployFastCache = {};
12
15
  const getCacheKey = (pg, name, database) => {
@@ -23,9 +26,12 @@ export const deployProject = async (opts, name, database, pkg, toChange) => {
23
26
  throw errors.MODULE_NOT_FOUND({ name });
24
27
  }
25
28
  const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
26
- const moduleProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
27
29
  log.info(`📦 Resolving dependencies for ${name}...`);
28
- const extensions = moduleProject.getModuleExtensions();
30
+ // Proxy (apply-spec) modules have no pgpm.plan, so resolve straight off the
31
+ // workspace module map instead of instantiating a module-rooted package.
32
+ const extensions = hasApplySpec(modulePath)
33
+ ? resolveExtensionDependencies(name, modules)
34
+ : new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir }).getModuleExtensions();
29
35
  const pgPool = getPgPool({ ...opts.pg, database });
30
36
  log.success(`🚀 Starting deployment to database ${database}...`);
31
37
  for (const extension of extensions.resolved) {
@@ -37,9 +43,13 @@ export const deployProject = async (opts, name, database, pkg, toChange) => {
37
43
  await pgPool.query(msg);
38
44
  }
39
45
  else {
40
- const modulePath = resolve(pkg.workspacePath, modules[extension].path);
46
+ const sourcePath = resolve(pkg.workspacePath, modules[extension].path);
47
+ const modulePath = await resolveEffectiveModulePath(extension, sourcePath, modules, pkg.workspacePath);
41
48
  log.info(`📂 Deploying local module: ${extension}`);
42
49
  log.debug(`→ Path: ${modulePath}`);
50
+ if (modulePath !== sourcePath) {
51
+ log.info(`🔁 Applying transpiled module (spec in ${sourcePath})`);
52
+ }
43
53
  if (mergedOpts.deployment.fast) {
44
54
  // Use fast deployment strategy
45
55
  const localProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
@@ -3,8 +3,11 @@ import { errors } from '@pgpmjs/types';
3
3
  import { resolve } from 'path';
4
4
  import * as path from 'path';
5
5
  import { getPgPool } from 'pg-cache';
6
+ import { resolveEffectiveModulePath } from '../apply/materialize';
7
+ import { hasApplySpec } from '../apply/apply-spec';
6
8
  import { PgpmPackage } from '../core/class/pgpm';
7
9
  import { PgpmMigrate } from '../migrate/client';
10
+ import { resolveExtensionDependencies } from '../resolution/deps';
8
11
  const log = new Logger('revert');
9
12
  export const revertProject = async (opts, name, database, pkg, options) => {
10
13
  log.info(`🔍 Gathering modules from ${pkg.workspacePath}...`);
@@ -14,9 +17,12 @@ export const revertProject = async (opts, name, database, pkg, options) => {
14
17
  throw errors.MODULE_NOT_FOUND({ name });
15
18
  }
16
19
  const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
17
- const moduleProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
18
20
  log.info(`📦 Resolving dependencies for ${name}...`);
19
- const extensions = moduleProject.getModuleExtensions();
21
+ // Proxy (apply-spec) modules have no pgpm.plan, so resolve straight off the
22
+ // workspace module map instead of instantiating a module-rooted package.
23
+ const extensions = hasApplySpec(modulePath)
24
+ ? resolveExtensionDependencies(name, modules)
25
+ : new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir }).getModuleExtensions();
20
26
  const pgPool = getPgPool({
21
27
  ...opts.pg,
22
28
  database
@@ -42,7 +48,8 @@ export const revertProject = async (opts, name, database, pkg, options) => {
42
48
  }
43
49
  }
44
50
  else {
45
- const modulePath = resolve(pkg.workspacePath, modules[extension].path);
51
+ const sourcePath = resolve(pkg.workspacePath, modules[extension].path);
52
+ const modulePath = await resolveEffectiveModulePath(extension, sourcePath, modules, pkg.workspacePath);
46
53
  log.info(`📂 Reverting local module: ${extension}`);
47
54
  log.debug(`→ Path: ${modulePath}`);
48
55
  // Use new migration system
@@ -3,8 +3,11 @@ import { errors } from '@pgpmjs/types';
3
3
  import { resolve } from 'path';
4
4
  import * as path from 'path';
5
5
  import { getPgPool } from 'pg-cache';
6
+ import { resolveEffectiveModulePath } from '../apply/materialize';
7
+ import { hasApplySpec } from '../apply/apply-spec';
6
8
  import { PgpmPackage } from '../core/class/pgpm';
7
9
  import { PgpmMigrate } from '../migrate/client';
10
+ import { resolveExtensionDependencies } from '../resolution/deps';
8
11
  const log = new Logger('verify');
9
12
  export const verifyProject = async (opts, name, database, pkg, options) => {
10
13
  log.info(`🔍 Gathering modules from ${pkg.workspacePath}...`);
@@ -14,9 +17,12 @@ export const verifyProject = async (opts, name, database, pkg, options) => {
14
17
  throw errors.MODULE_NOT_FOUND({ name });
15
18
  }
16
19
  const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
17
- const moduleProject = new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
18
20
  log.info(`📦 Resolving dependencies for ${name}...`);
19
- const extensions = moduleProject.getModuleExtensions();
21
+ // Proxy (apply-spec) modules have no pgpm.plan, so resolve straight off the
22
+ // workspace module map instead of instantiating a module-rooted package.
23
+ const extensions = hasApplySpec(modulePath)
24
+ ? resolveExtensionDependencies(name, modules)
25
+ : new PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir }).getModuleExtensions();
20
26
  const pgPool = getPgPool({
21
27
  ...opts.pg,
22
28
  database
@@ -31,7 +37,8 @@ export const verifyProject = async (opts, name, database, pkg, options) => {
31
37
  await pgPool.query(query, [extension]);
32
38
  }
33
39
  else {
34
- const modulePath = resolve(pkg.workspacePath, modules[extension].path);
40
+ const sourcePath = resolve(pkg.workspacePath, modules[extension].path);
41
+ const modulePath = await resolveEffectiveModulePath(extension, sourcePath, modules, pkg.workspacePath);
35
42
  log.info(`📂 Verifying local module: ${extension}`);
36
43
  log.debug(`→ Path: ${modulePath}`);
37
44
  log.debug(`→ Command: constructive migrate verify db:pg:${database}`);
package/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from './core/boilerplate-types';
15
15
  export * from './core/boilerplate-scanner';
16
16
  export * from '@pgpmjs/ast/files';
17
17
  export * from '@pgpmjs/ast/module';
18
+ export * from './apply';
18
19
  export * from './bundle';
19
20
  export * from './refactor';
20
21
  export { cleanSql } from './migrate/clean';
package/index.js CHANGED
@@ -33,6 +33,7 @@ __exportStar(require("./core/boilerplate-scanner"), exports);
33
33
  // Re-export the module AST layer (moved to the @pgpmjs/ast leaf package)
34
34
  __exportStar(require("@pgpmjs/ast/files"), exports);
35
35
  __exportStar(require("@pgpmjs/ast/module"), exports);
36
+ __exportStar(require("./apply"), exports);
36
37
  __exportStar(require("./bundle"), exports);
37
38
  __exportStar(require("./refactor"), exports);
38
39
  var clean_1 = require("./migrate/clean");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpmjs/core",
3
- "version": "7.11.1",
3
+ "version": "7.14.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PGPM Package and Migration Tools",
6
6
  "main": "index.js",
@@ -48,23 +48,24 @@
48
48
  "makage": "^0.3.0"
49
49
  },
50
50
  "dependencies": {
51
- "@pgpmjs/ast": "^0.7.1",
52
- "@pgpmjs/bundle": "^0.8.1",
53
- "@pgpmjs/env": "^2.35.1",
54
- "@pgpmjs/logger": "^2.20.0",
55
- "@pgpmjs/server-utils": "^3.21.1",
56
- "@pgpmjs/slice": "^0.7.1",
57
- "@pgpmjs/types": "^2.43.1",
58
- "csv-to-pg": "^4.6.0",
51
+ "@pgpmjs/ast": "^0.9.1",
52
+ "@pgpmjs/bundle": "^0.11.0",
53
+ "@pgpmjs/env": "^2.37.0",
54
+ "@pgpmjs/logger": "^2.22.0",
55
+ "@pgpmjs/server-utils": "^3.23.0",
56
+ "@pgpmjs/slice": "^0.10.0",
57
+ "@pgpmjs/transform": "^0.10.0",
58
+ "@pgpmjs/types": "^2.45.0",
59
+ "csv-to-pg": "^4.8.0",
59
60
  "genomic": "^5.6.2",
60
61
  "glob": "^13.0.6",
61
62
  "parse-package-name": "^1.0.0",
62
63
  "pg": "^8.21.0",
63
- "pg-cache": "^3.21.1",
64
- "pg-env": "^1.24.0",
64
+ "pg-cache": "^3.23.0",
65
+ "pg-env": "^1.26.0",
65
66
  "pgsql-deparser": "^18.1.1",
66
67
  "pgsql-parser": "^18.1.1",
67
68
  "yanse": "^0.2.1"
68
69
  },
69
- "gitHead": "37e83eb66e9d13d0d4d8ac3b36b5ebe394b84a43"
70
+ "gitHead": "d85911699e8a50a9fdfeb7506195442525c1f238"
70
71
  }
@@ -40,9 +40,12 @@ const types_1 = require("@pgpmjs/types");
40
40
  const path_1 = require("path");
41
41
  const path = __importStar(require("path"));
42
42
  const pg_cache_1 = require("pg-cache");
43
+ const materialize_1 = require("../apply/materialize");
44
+ const apply_spec_1 = require("../apply/apply-spec");
43
45
  const pgpm_1 = require("../core/class/pgpm");
44
46
  const client_1 = require("../migrate/client");
45
47
  const package_1 = require("../packaging/package");
48
+ const deps_1 = require("../resolution/deps");
46
49
  // Cache for fast deployment
47
50
  const deployFastCache = {};
48
51
  const getCacheKey = (pg, name, database) => {
@@ -59,9 +62,12 @@ const deployProject = async (opts, name, database, pkg, toChange) => {
59
62
  throw types_1.errors.MODULE_NOT_FOUND({ name });
60
63
  }
61
64
  const modulePath = path.resolve(pkg.workspacePath, modules[name].path);
62
- const moduleProject = new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });
63
65
  log.info(`📦 Resolving dependencies for ${name}...`);
64
- const extensions = moduleProject.getModuleExtensions();
66
+ // Proxy (apply-spec) modules have no pgpm.plan, so resolve straight off the
67
+ // workspace module map instead of instantiating a module-rooted package.
68
+ const extensions = (0, apply_spec_1.hasApplySpec)(modulePath)
69
+ ? (0, deps_1.resolveExtensionDependencies)(name, modules)
70
+ : new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir }).getModuleExtensions();
65
71
  const pgPool = (0, pg_cache_1.getPgPool)({ ...opts.pg, database });
66
72
  log.success(`🚀 Starting deployment to database ${database}...`);
67
73
  for (const extension of extensions.resolved) {
@@ -73,9 +79,13 @@ const deployProject = async (opts, name, database, pkg, toChange) => {
73
79
  await pgPool.query(msg);
74
80
  }
75
81
  else {
76
- const modulePath = (0, path_1.resolve)(pkg.workspacePath, modules[extension].path);
82
+ const sourcePath = (0, path_1.resolve)(pkg.workspacePath, modules[extension].path);
83
+ const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, sourcePath, modules, pkg.workspacePath);
77
84
  log.info(`📂 Deploying local module: ${extension}`);
78
85
  log.debug(`→ Path: ${modulePath}`);
86
+ if (modulePath !== sourcePath) {
87
+ log.info(`🔁 Applying transpiled module (spec in ${sourcePath})`);
88
+ }
79
89
  if (mergedOpts.deployment.fast) {
80
90
  // Use fast deployment strategy
81
91
  const localProject = new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir });