@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.
- package/apply/apply-spec.d.ts +14 -0
- package/apply/apply-spec.js +90 -0
- package/apply/index.d.ts +3 -0
- package/apply/index.js +19 -0
- package/apply/materialize.d.ts +40 -0
- package/apply/materialize.js +121 -0
- package/apply/types.d.ts +83 -0
- package/apply/types.js +17 -0
- package/core/class/pgpm.d.ts +20 -0
- package/core/class/pgpm.js +78 -16
- package/esm/apply/apply-spec.js +85 -0
- package/esm/apply/index.js +3 -0
- package/esm/apply/materialize.js +116 -0
- package/esm/apply/types.js +14 -0
- package/esm/core/class/pgpm.js +78 -16
- package/esm/index.js +1 -0
- package/esm/projects/deploy.js +13 -3
- package/esm/projects/revert.js +10 -3
- package/esm/projects/verify.js +10 -3
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +13 -12
- package/projects/deploy.js +13 -3
- package/projects/revert.js +10 -3
- package/projects/verify.js +10 -3
|
@@ -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,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';
|
package/esm/core/class/pgpm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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';
|
package/esm/projects/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 });
|
package/esm/projects/revert.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
package/esm/projects/verify.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
52
|
-
"@pgpmjs/bundle": "^0.
|
|
53
|
-
"@pgpmjs/env": "^2.
|
|
54
|
-
"@pgpmjs/logger": "^2.
|
|
55
|
-
"@pgpmjs/server-utils": "^3.
|
|
56
|
-
"@pgpmjs/slice": "^0.
|
|
57
|
-
"@pgpmjs/
|
|
58
|
-
"
|
|
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.
|
|
64
|
-
"pg-env": "^1.
|
|
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": "
|
|
70
|
+
"gitHead": "d85911699e8a50a9fdfeb7506195442525c1f238"
|
|
70
71
|
}
|
package/projects/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 });
|