@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,14 @@
|
|
|
1
|
+
import { ResolvedApplySpec } from './types';
|
|
2
|
+
/** Whether a module directory is an apply-spec proxy module. */
|
|
3
|
+
export declare function hasApplySpec(moduleDir: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Read, validate, and normalize a proxy module's `pgpm.apply.json`.
|
|
6
|
+
*
|
|
7
|
+
* Normalizations: `source` is expanded to its object form and `name` defaults
|
|
8
|
+
* to the proxy directory's name.
|
|
9
|
+
*
|
|
10
|
+
* @throws when the file is missing, unparsable, or structurally invalid.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readApplySpec(moduleDir: string): ResolvedApplySpec;
|
|
13
|
+
/** In-memory core of {@link readApplySpec}. */
|
|
14
|
+
export declare function parseApplySpec(content: string, specPath: string): ResolvedApplySpec;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasApplySpec = hasApplySpec;
|
|
4
|
+
exports.readApplySpec = readApplySpec;
|
|
5
|
+
exports.parseApplySpec = parseApplySpec;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
/** Whether a module directory is an apply-spec proxy module. */
|
|
10
|
+
function hasApplySpec(moduleDir) {
|
|
11
|
+
return (0, fs_1.existsSync)((0, path_1.join)(moduleDir, types_1.APPLY_SPEC_FILE));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read, validate, and normalize a proxy module's `pgpm.apply.json`.
|
|
15
|
+
*
|
|
16
|
+
* Normalizations: `source` is expanded to its object form and `name` defaults
|
|
17
|
+
* to the proxy directory's name.
|
|
18
|
+
*
|
|
19
|
+
* @throws when the file is missing, unparsable, or structurally invalid.
|
|
20
|
+
*/
|
|
21
|
+
function readApplySpec(moduleDir) {
|
|
22
|
+
const specPath = (0, path_1.join)(moduleDir, types_1.APPLY_SPEC_FILE);
|
|
23
|
+
if (!(0, fs_1.existsSync)(specPath)) {
|
|
24
|
+
throw new Error(`No ${types_1.APPLY_SPEC_FILE} found in ${moduleDir}`);
|
|
25
|
+
}
|
|
26
|
+
return parseApplySpec((0, fs_1.readFileSync)(specPath, 'utf-8'), specPath);
|
|
27
|
+
}
|
|
28
|
+
/** In-memory core of {@link readApplySpec}. */
|
|
29
|
+
function parseApplySpec(content, specPath) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error(`Failed to parse ${specPath}: ${err instanceof Error ? err.message : err}`);
|
|
36
|
+
}
|
|
37
|
+
const source = typeof parsed?.source === 'string' ? { module: parsed.source } : parsed?.source;
|
|
38
|
+
if (!source || typeof source.module !== 'string' || !source.module) {
|
|
39
|
+
throw new Error(`${specPath}: "source" (source module name, or { "module": ... }) is required`);
|
|
40
|
+
}
|
|
41
|
+
const hasSchemas = parsed.schemas !== undefined;
|
|
42
|
+
if (hasSchemas) {
|
|
43
|
+
if (typeof parsed.schemas !== 'object' ||
|
|
44
|
+
Array.isArray(parsed.schemas) ||
|
|
45
|
+
Object.keys(parsed.schemas).length === 0 ||
|
|
46
|
+
Object.entries(parsed.schemas).some(([from, to]) => typeof from !== 'string' || typeof to !== 'string' || !from || !to)) {
|
|
47
|
+
throw new Error(`${specPath}: "schemas" must be a non-empty string → string map`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const ROUTE_KINDS = ['table', 'view', 'function', 'procedure', 'type'];
|
|
51
|
+
const hasRoute = parsed.route !== undefined;
|
|
52
|
+
if (hasRoute) {
|
|
53
|
+
if (!Array.isArray(parsed.route) || parsed.route.length === 0) {
|
|
54
|
+
throw new Error(`${specPath}: "route" must be a non-empty array of route entries`);
|
|
55
|
+
}
|
|
56
|
+
for (const entry of parsed.route) {
|
|
57
|
+
if (!entry ||
|
|
58
|
+
typeof entry !== 'object' ||
|
|
59
|
+
typeof entry.fromSchema !== 'string' ||
|
|
60
|
+
!entry.fromSchema ||
|
|
61
|
+
typeof entry.name !== 'string' ||
|
|
62
|
+
!entry.name ||
|
|
63
|
+
typeof entry.toSchema !== 'string' ||
|
|
64
|
+
!entry.toSchema ||
|
|
65
|
+
!ROUTE_KINDS.includes(entry.kind)) {
|
|
66
|
+
throw new Error(`${specPath}: each "route" entry needs { fromSchema, kind (${ROUTE_KINDS.join('|')}), name, toSchema } as non-empty strings`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!hasSchemas && !hasRoute) {
|
|
71
|
+
throw new Error(`${specPath}: at least one of "schemas" (string → string map) or "route" (object routes) is required`);
|
|
72
|
+
}
|
|
73
|
+
const name = parsed.name ?? (0, path_1.basename)((0, path_1.dirname)(specPath));
|
|
74
|
+
if (typeof name !== 'string' || !name) {
|
|
75
|
+
throw new Error(`${specPath}: "name" must be a non-empty string`);
|
|
76
|
+
}
|
|
77
|
+
if (name === source.module) {
|
|
78
|
+
throw new Error(`${specPath}: apply module "${name}" cannot apply itself`);
|
|
79
|
+
}
|
|
80
|
+
const requires = parsed.requires;
|
|
81
|
+
if (requires !== undefined &&
|
|
82
|
+
(!Array.isArray(requires) || requires.some((r) => typeof r !== 'string' || !r))) {
|
|
83
|
+
throw new Error(`${specPath}: "requires" must be an array of module names`);
|
|
84
|
+
}
|
|
85
|
+
if (requires?.includes(source.module)) {
|
|
86
|
+
throw new Error(`${specPath}: "requires" must not include the source module "${source.module}" — ` +
|
|
87
|
+
`it is source material, not a runtime dependency`);
|
|
88
|
+
}
|
|
89
|
+
return { ...parsed, name, source, requires };
|
|
90
|
+
}
|
package/apply/index.d.ts
ADDED
package/apply/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./materialize"), exports);
|
|
18
|
+
__exportStar(require("./apply-spec"), exports);
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MigrationBundle } from '@pgpmjs/bundle';
|
|
2
|
+
import { ModuleMap } from '../modules/modules';
|
|
3
|
+
import { ResolvedApplySpec } from './types';
|
|
4
|
+
export interface MaterializeApplyOptions {
|
|
5
|
+
/** Directory of the source module being applied. */
|
|
6
|
+
sourceDir: string;
|
|
7
|
+
/** The apply spec (typically read from the proxy via {@link readApplySpec}). */
|
|
8
|
+
spec: ResolvedApplySpec;
|
|
9
|
+
/** Directory to materialize into (default: a fresh temp dir). */
|
|
10
|
+
outDir?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MaterializeApplyResult {
|
|
13
|
+
/** The transpiled, content-addressed bundle that was materialized. */
|
|
14
|
+
bundle: MigrationBundle;
|
|
15
|
+
/** The deployable module directory the bundle was written to. */
|
|
16
|
+
outDir: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Transpile a source module per an apply spec and materialize the result as a
|
|
20
|
+
* deployable module directory carrying the instance's identity.
|
|
21
|
+
*
|
|
22
|
+
* Pure pipeline over existing primitives: `bundleFromModule` →
|
|
23
|
+
* `transpileBundle(makeSchemaTranspiler)` → `verifyBundle` →
|
|
24
|
+
* `materializeBundle`. Deterministic — identical source + spec always produce
|
|
25
|
+
* an identical bundle (and digest), which is what makes verify/revert able to
|
|
26
|
+
* re-derive the exact deployed artifact.
|
|
27
|
+
*/
|
|
28
|
+
export declare function materializeApplyModule(options: MaterializeApplyOptions): Promise<MaterializeApplyResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Deploy-path dispatch seam: given a resolved module's directory, return the
|
|
31
|
+
* directory the migration engine should actually run against.
|
|
32
|
+
*
|
|
33
|
+
* Non-proxy modules pass through unchanged. For proxy modules (directories
|
|
34
|
+
* carrying a `pgpm.apply.json`), the source module is located in the workspace
|
|
35
|
+
* module map, transpiled per the spec, and materialized to a cached temp dir
|
|
36
|
+
* carrying the instance's identity.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveEffectiveModulePath(moduleName: string, modulePath: string, moduleMap: ModuleMap, workspacePath: string): Promise<string>;
|
|
39
|
+
/** Test seam: clear the per-process materialization cache. */
|
|
40
|
+
export declare function clearApplyMaterializationCache(): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.materializeApplyModule = materializeApplyModule;
|
|
4
|
+
exports.resolveEffectiveModulePath = resolveEffectiveModulePath;
|
|
5
|
+
exports.clearApplyMaterializationCache = clearApplyMaterializationCache;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const bundle_1 = require("@pgpmjs/bundle");
|
|
10
|
+
const transform_1 = require("@pgpmjs/transform");
|
|
11
|
+
const apply_spec_1 = require("./apply-spec");
|
|
12
|
+
// Functions that take a bare schema name as a string-literal first argument.
|
|
13
|
+
// The AST pass only remaps dotted literals ('schema.object'); these helpers
|
|
14
|
+
// (ubiquitous in verify scripts) name the schema alone, so a string-level
|
|
15
|
+
// pre-pass remaps them.
|
|
16
|
+
const SCHEMA_NAME_LITERAL_FUNCS = ['verify_schema', 'has_schema_privilege'];
|
|
17
|
+
const schemaNameLiteralPass = (content, schemaMapping) => {
|
|
18
|
+
const pattern = new RegExp(`\\b(${SCHEMA_NAME_LITERAL_FUNCS.join('|')})\\s*\\(\\s*'([^']+)'`, 'gi');
|
|
19
|
+
return content.replace(pattern, (match, _fn, schema) => {
|
|
20
|
+
const mapped = schemaMapping.get(schema);
|
|
21
|
+
return mapped ? match.replace(`'${schema}'`, `'${mapped}'`) : match;
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Transpile a source module per an apply spec and materialize the result as a
|
|
26
|
+
* deployable module directory carrying the instance's identity.
|
|
27
|
+
*
|
|
28
|
+
* Pure pipeline over existing primitives: `bundleFromModule` →
|
|
29
|
+
* `transpileBundle(makeSchemaTranspiler)` → `verifyBundle` →
|
|
30
|
+
* `materializeBundle`. Deterministic — identical source + spec always produce
|
|
31
|
+
* an identical bundle (and digest), which is what makes verify/revert able to
|
|
32
|
+
* re-derive the exact deployed artifact.
|
|
33
|
+
*/
|
|
34
|
+
async function materializeApplyModule(options) {
|
|
35
|
+
const { sourceDir, spec } = options;
|
|
36
|
+
const instanceName = spec.name;
|
|
37
|
+
// the schema transpiler's SQL/PLpgSQL parser is WASM-backed and needs a
|
|
38
|
+
// one-time async init (idempotent)
|
|
39
|
+
await (0, transform_1.loadModule)();
|
|
40
|
+
const source = (0, bundle_1.bundleFromModule)(sourceDir);
|
|
41
|
+
if (spec.source.bundleDigest && spec.source.bundleDigest !== source.manifest.digest) {
|
|
42
|
+
throw new Error(`Apply spec for "${instanceName}" pins source bundle digest ${spec.source.bundleDigest}, ` +
|
|
43
|
+
`but the installed source "${spec.source.module}" hashes to ${source.manifest.digest}. ` +
|
|
44
|
+
`Reinstall the pinned version or update the spec.`);
|
|
45
|
+
}
|
|
46
|
+
// Every distinct target schema is treated as "may already exist": apply
|
|
47
|
+
// deploys into consumer-chosen namespaces that can pre-exist (a shared schema,
|
|
48
|
+
// another tenant), so `CREATE SCHEMA` is emitted idempotently. This is what
|
|
49
|
+
// lets the same source apply cleanly whether or not the destination exists.
|
|
50
|
+
const targetSchemas = [
|
|
51
|
+
...new Set([
|
|
52
|
+
...Object.values(spec.schemas ?? {}),
|
|
53
|
+
...(spec.route ?? []).map(r => r.toSchema)
|
|
54
|
+
])
|
|
55
|
+
];
|
|
56
|
+
const { renameChange, transformScript, result } = (0, transform_1.makeSchemaTranspiler)({
|
|
57
|
+
schemaMap: spec.schemas,
|
|
58
|
+
routes: spec.route,
|
|
59
|
+
transform: {
|
|
60
|
+
prePasses: [schemaNameLiteralPass],
|
|
61
|
+
assumeSchemasExist: targetSchemas
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const transpiled = (0, bundle_1.transpileBundle)(source, {
|
|
65
|
+
renameChange,
|
|
66
|
+
transformScript,
|
|
67
|
+
renameModule: instanceName,
|
|
68
|
+
provenance: {
|
|
69
|
+
appliedFrom: spec.source.module,
|
|
70
|
+
...(spec.source.package ? { sourcePackage: spec.source.package } : {}),
|
|
71
|
+
...(spec.source.version ? { sourceVersion: spec.source.version } : {})
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (result.errors.length > 0) {
|
|
75
|
+
const detail = result.errors.map(e => `${e.file}: ${e.error}`).join('; ');
|
|
76
|
+
throw new Error(`Apply transpile of "${spec.source.module}" as "${instanceName}" failed: ${detail}`);
|
|
77
|
+
}
|
|
78
|
+
const issues = (0, bundle_1.verifyBundle)(transpiled);
|
|
79
|
+
if (issues.length > 0) {
|
|
80
|
+
throw new Error(`Transpiled bundle for "${instanceName}" failed integrity verification: ` +
|
|
81
|
+
issues.map(i => i.kind).join(', '));
|
|
82
|
+
}
|
|
83
|
+
const outDir = options.outDir ?? (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), `pgpm-apply-${instanceName}-`));
|
|
84
|
+
(0, bundle_1.materializeBundle)(transpiled, outDir);
|
|
85
|
+
return { bundle: transpiled, outDir };
|
|
86
|
+
}
|
|
87
|
+
// One materialization per proxy path per process: the pipeline is
|
|
88
|
+
// deterministic, so re-deriving within a process is pure waste.
|
|
89
|
+
const materializedCache = new Map();
|
|
90
|
+
/**
|
|
91
|
+
* Deploy-path dispatch seam: given a resolved module's directory, return the
|
|
92
|
+
* directory the migration engine should actually run against.
|
|
93
|
+
*
|
|
94
|
+
* Non-proxy modules pass through unchanged. For proxy modules (directories
|
|
95
|
+
* carrying a `pgpm.apply.json`), the source module is located in the workspace
|
|
96
|
+
* module map, transpiled per the spec, and materialized to a cached temp dir
|
|
97
|
+
* carrying the instance's identity.
|
|
98
|
+
*/
|
|
99
|
+
async function resolveEffectiveModulePath(moduleName, modulePath, moduleMap, workspacePath) {
|
|
100
|
+
if (!(0, apply_spec_1.hasApplySpec)(modulePath))
|
|
101
|
+
return modulePath;
|
|
102
|
+
const cached = materializedCache.get(modulePath);
|
|
103
|
+
if (cached)
|
|
104
|
+
return cached;
|
|
105
|
+
const spec = (0, apply_spec_1.readApplySpec)(modulePath);
|
|
106
|
+
const sourceModule = moduleMap[spec.source.module];
|
|
107
|
+
if (!sourceModule) {
|
|
108
|
+
throw new Error(`Apply spec in "${moduleName}" references source module "${spec.source.module}", ` +
|
|
109
|
+
`which was not found in the workspace. Run \`pgpm install\` to install it` +
|
|
110
|
+
(spec.source.package ? ` (${spec.source.package})` : '') +
|
|
111
|
+
`.`);
|
|
112
|
+
}
|
|
113
|
+
const sourceDir = (0, path_1.resolve)(workspacePath, sourceModule.path);
|
|
114
|
+
const { outDir } = await materializeApplyModule({ sourceDir, spec });
|
|
115
|
+
materializedCache.set(modulePath, outDir);
|
|
116
|
+
return outDir;
|
|
117
|
+
}
|
|
118
|
+
/** Test seam: clear the per-process materialization cache. */
|
|
119
|
+
function clearApplyMaterializationCache() {
|
|
120
|
+
materializedCache.clear();
|
|
121
|
+
}
|
package/apply/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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 declare const APPLY_SPEC_FILE = "pgpm.apply.json";
|
|
15
|
+
/** Expanded reference to the source module a proxy module applies. */
|
|
16
|
+
export interface ApplySpecSource {
|
|
17
|
+
/**
|
|
18
|
+
* Control/module name of the source module as it appears in the workspace
|
|
19
|
+
* module map (e.g. `pgpm-totp`). The source is typically installed into the
|
|
20
|
+
* workspace extensions directory via `pgpm install`; it is source material,
|
|
21
|
+
* not a runtime dependency, so it must not appear in `requires`.
|
|
22
|
+
*/
|
|
23
|
+
module: string;
|
|
24
|
+
/** Optional npm package name the source is installed from (e.g. `@pgpm/totp`). */
|
|
25
|
+
package?: string;
|
|
26
|
+
/** Optional exact version pin for the source package. */
|
|
27
|
+
version?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional content-addressed pin: the expected `manifest.digest` of the
|
|
30
|
+
* source bundle. When present, apply refuses to proceed if the installed
|
|
31
|
+
* source does not hash to exactly this digest.
|
|
32
|
+
*/
|
|
33
|
+
bundleDigest?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An object-level route in a `pgpm.apply.json` spec: send one named object out
|
|
37
|
+
* of a source schema to a target schema, overriding the whole-schema default
|
|
38
|
+
* for that object only. Kinds are expressed as separate properties (no dotted
|
|
39
|
+
* identity strings), so a table and a function of the same name route
|
|
40
|
+
* independently.
|
|
41
|
+
*/
|
|
42
|
+
export interface ApplyRouteEntry {
|
|
43
|
+
/** Source schema the object is defined in (e.g. `users`). */
|
|
44
|
+
fromSchema: string;
|
|
45
|
+
/** Object namespace: `table`/`view` → relation, `procedure` → function. */
|
|
46
|
+
kind: 'table' | 'view' | 'function' | 'procedure' | 'type';
|
|
47
|
+
/** Unqualified object name (e.g. `accounts`). */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Target schema the object is routed to (e.g. `reporting`). */
|
|
50
|
+
toSchema: string;
|
|
51
|
+
}
|
|
52
|
+
/** The parsed, normalized `pgpm.apply.json` spec. */
|
|
53
|
+
export interface PgpmApplySpec {
|
|
54
|
+
/**
|
|
55
|
+
* Module name this instance is deployed as. Defaults to the proxy
|
|
56
|
+
* directory's name.
|
|
57
|
+
*/
|
|
58
|
+
name?: string;
|
|
59
|
+
/** Source module reference: a module name, or the expanded object form. */
|
|
60
|
+
source: string | ApplySpecSource;
|
|
61
|
+
/**
|
|
62
|
+
* Whole-schema default: source schema → target schema (drives the AST + plan
|
|
63
|
+
* rewrite). Optional when `route` fully covers the objects being moved.
|
|
64
|
+
*/
|
|
65
|
+
schemas?: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Object-level routes. Each entry overrides the `schemas` default for its
|
|
68
|
+
* specific object, letting one source schema fan out per object (e.g. a
|
|
69
|
+
* table into a tenant schema and a function into a shared schema).
|
|
70
|
+
*/
|
|
71
|
+
route?: ApplyRouteEntry[];
|
|
72
|
+
/**
|
|
73
|
+
* Runtime requires of the transpiled output (native extensions and other
|
|
74
|
+
* modules). Defaults to the source module's own requires.
|
|
75
|
+
*/
|
|
76
|
+
requires?: string[];
|
|
77
|
+
/** Version reported for this instance (default: the source's version). */
|
|
78
|
+
version?: string;
|
|
79
|
+
}
|
|
80
|
+
/** {@link PgpmApplySpec} with `source` normalized to the object form. */
|
|
81
|
+
export interface ResolvedApplySpec extends Omit<PgpmApplySpec, 'source'> {
|
|
82
|
+
source: ApplySpecSource;
|
|
83
|
+
}
|
package/apply/types.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The apply-spec ("proxy module") types.
|
|
4
|
+
*
|
|
5
|
+
* A proxy module is a directory containing a single `pgpm.apply.json` file:
|
|
6
|
+
* "apply <source module> transpiled with this schema map, under my name". It
|
|
7
|
+
* needs no `.control`, no `pgpm.plan`, and no deploy/revert/verify scripts —
|
|
8
|
+
* workspace discovery synthesizes a module entry from the spec, so the proxy
|
|
9
|
+
* participates in dependency resolution by name exactly like a regular
|
|
10
|
+
* module. At deploy/verify/revert time the engine transpiles the source
|
|
11
|
+
* module in memory and runs the normal migration path against the result;
|
|
12
|
+
* transpiled code is never committed.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.APPLY_SPEC_FILE = void 0;
|
|
16
|
+
/** File name of the apply spec inside a proxy module directory. */
|
|
17
|
+
exports.APPLY_SPEC_FILE = 'pgpm.apply.json';
|
package/core/class/pgpm.d.ts
CHANGED
|
@@ -97,6 +97,14 @@ export declare class PgpmPackage {
|
|
|
97
97
|
* Handles naming collisions by preferring the shortest path.
|
|
98
98
|
*/
|
|
99
99
|
listModules(): ModuleMap;
|
|
100
|
+
/**
|
|
101
|
+
* Discover apply-spec proxy modules (`pgpm.apply.json`) and synthesize
|
|
102
|
+
* module-map entries for them so they participate in dependency resolution
|
|
103
|
+
* by name like any other module. A proxy needs no `.control` or `pgpm.plan`;
|
|
104
|
+
* its `requires` default to the source module's requires. Control-file
|
|
105
|
+
* modules win name collisions.
|
|
106
|
+
*/
|
|
107
|
+
private addApplyModules;
|
|
100
108
|
getModuleMap(): ModuleMap;
|
|
101
109
|
getAvailableModules(): Promise<string[]>;
|
|
102
110
|
getModuleProject(name: string): PgpmPackage;
|
|
@@ -271,6 +279,18 @@ export declare class PgpmPackage {
|
|
|
271
279
|
resolved: string[];
|
|
272
280
|
external: string[];
|
|
273
281
|
}>;
|
|
282
|
+
/**
|
|
283
|
+
* Dependency closure for a named module. Proxy (apply-spec) modules have no
|
|
284
|
+
* pgpm.plan, so they resolve straight off the workspace module map instead
|
|
285
|
+
* of instantiating a module-rooted package.
|
|
286
|
+
*/
|
|
287
|
+
private getModuleExtensionsFor;
|
|
288
|
+
/**
|
|
289
|
+
* Directory the migration engine should run against for a named module:
|
|
290
|
+
* the module's own directory, or — for proxy (apply-spec) modules — the
|
|
291
|
+
* materialized transpiled instance.
|
|
292
|
+
*/
|
|
293
|
+
private getEffectiveModulePathFor;
|
|
274
294
|
private parsePackageTarget;
|
|
275
295
|
deploy(opts: PgpmOptions, target?: string, recursive?: boolean): Promise<void>;
|
|
276
296
|
/**
|
package/core/class/pgpm.js
CHANGED
|
@@ -48,6 +48,9 @@ const parse_package_name_1 = require("parse-package-name");
|
|
|
48
48
|
const path_1 = __importStar(require("path"));
|
|
49
49
|
const pg_cache_1 = require("pg-cache");
|
|
50
50
|
const yanse_1 = __importDefault(require("yanse"));
|
|
51
|
+
const materialize_1 = require("../../apply/materialize");
|
|
52
|
+
const apply_spec_1 = require("../../apply/apply-spec");
|
|
53
|
+
const types_2 = require("../../apply/types");
|
|
51
54
|
const extensions_1 = require("../../extensions/extensions");
|
|
52
55
|
const files_1 = require("@pgpmjs/ast/files");
|
|
53
56
|
const files_2 = require("@pgpmjs/ast/files");
|
|
@@ -281,11 +284,44 @@ class PgpmPackage {
|
|
|
281
284
|
}
|
|
282
285
|
});
|
|
283
286
|
// Parse the selected control files
|
|
284
|
-
|
|
287
|
+
const moduleMap = Array.from(selectedFiles.entries()).reduce((acc, [moduleName, file]) => {
|
|
285
288
|
const module = (0, files_2.parseControlFile)(file, this.workspacePath);
|
|
286
289
|
acc[moduleName] = module;
|
|
287
290
|
return acc;
|
|
288
291
|
}, {});
|
|
292
|
+
return this.addApplyModules(moduleMap, packageDirs);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Discover apply-spec proxy modules (`pgpm.apply.json`) and synthesize
|
|
296
|
+
* module-map entries for them so they participate in dependency resolution
|
|
297
|
+
* by name like any other module. A proxy needs no `.control` or `pgpm.plan`;
|
|
298
|
+
* its `requires` default to the source module's requires. Control-file
|
|
299
|
+
* modules win name collisions.
|
|
300
|
+
*/
|
|
301
|
+
addApplyModules(moduleMap, packageDirs) {
|
|
302
|
+
const specFiles = [...new Set(packageDirs.flatMap(dir => glob.sync((0, glob_1.globPattern)(dir, `**/${types_2.APPLY_SPEC_FILE}`))))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
303
|
+
for (const file of specFiles) {
|
|
304
|
+
let spec;
|
|
305
|
+
try {
|
|
306
|
+
spec = (0, apply_spec_1.readApplySpec)(path_1.default.dirname(file));
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
console.warn(`Skipping invalid apply spec ${file}: ${err instanceof Error ? err.message : err}`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const name = spec.name;
|
|
313
|
+
if (moduleMap[name])
|
|
314
|
+
continue;
|
|
315
|
+
const source = moduleMap[spec.source.module];
|
|
316
|
+
moduleMap[name] = {
|
|
317
|
+
path: path_1.default
|
|
318
|
+
.dirname(path_1.default.relative(this.workspacePath, file))
|
|
319
|
+
.replace(/\\/g, '/'),
|
|
320
|
+
requires: spec.requires ?? source?.requires ?? [],
|
|
321
|
+
version: spec.version ?? source?.version ?? '0.0.1'
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return moduleMap;
|
|
289
325
|
}
|
|
290
326
|
getModuleMap() {
|
|
291
327
|
if (!this.workspacePath)
|
|
@@ -1235,6 +1271,38 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1235
1271
|
external: external
|
|
1236
1272
|
};
|
|
1237
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Dependency closure for a named module. Proxy (apply-spec) modules have no
|
|
1276
|
+
* pgpm.plan, so they resolve straight off the workspace module map instead
|
|
1277
|
+
* of instantiating a module-rooted package.
|
|
1278
|
+
*/
|
|
1279
|
+
getModuleExtensionsFor(name) {
|
|
1280
|
+
const modules = this.getModuleMap();
|
|
1281
|
+
if (!modules[name]) {
|
|
1282
|
+
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
1283
|
+
}
|
|
1284
|
+
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[name].path);
|
|
1285
|
+
if ((0, apply_spec_1.hasApplySpec)(modulePath)) {
|
|
1286
|
+
return (0, deps_1.resolveExtensionDependencies)(name, modules);
|
|
1287
|
+
}
|
|
1288
|
+
return this.getModuleProject(name).getModuleExtensions();
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Directory the migration engine should run against for a named module:
|
|
1292
|
+
* the module's own directory, or — for proxy (apply-spec) modules — the
|
|
1293
|
+
* materialized transpiled instance.
|
|
1294
|
+
*/
|
|
1295
|
+
async getEffectiveModulePathFor(name) {
|
|
1296
|
+
const modules = this.getModuleMap();
|
|
1297
|
+
if (!modules[name]) {
|
|
1298
|
+
throw types_1.errors.MODULE_NOT_FOUND({ name });
|
|
1299
|
+
}
|
|
1300
|
+
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[name].path);
|
|
1301
|
+
if ((0, apply_spec_1.hasApplySpec)(modulePath)) {
|
|
1302
|
+
return (0, materialize_1.resolveEffectiveModulePath)(name, modulePath, modules, this.workspacePath);
|
|
1303
|
+
}
|
|
1304
|
+
return this.getModuleProject(name).getModulePath();
|
|
1305
|
+
}
|
|
1238
1306
|
parsePackageTarget(target) {
|
|
1239
1307
|
let name;
|
|
1240
1308
|
let toChange;
|
|
@@ -1279,8 +1347,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1279
1347
|
extensions = await this.resolveWorkspaceExtensionDependencies();
|
|
1280
1348
|
}
|
|
1281
1349
|
else {
|
|
1282
|
-
|
|
1283
|
-
extensions = moduleProject.getModuleExtensions();
|
|
1350
|
+
extensions = this.getModuleExtensionsFor(name);
|
|
1284
1351
|
}
|
|
1285
1352
|
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
1286
1353
|
const targetDescription = name === null ? 'all modules' : name;
|
|
@@ -1293,10 +1360,9 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1293
1360
|
await pgPool.query(msg);
|
|
1294
1361
|
}
|
|
1295
1362
|
else {
|
|
1296
|
-
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
|
|
1363
|
+
const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, (0, path_1.resolve)(this.workspacePath, modules[extension].path), modules, this.workspacePath);
|
|
1297
1364
|
log.info(`📂 Deploying local module: ${extension}`);
|
|
1298
1365
|
if (opts.deployment.fast) {
|
|
1299
|
-
const localProject = this.getModuleProject(extension);
|
|
1300
1366
|
const cacheKey = getCacheKey(opts.pg, extension, opts.pg.database);
|
|
1301
1367
|
if (opts.deployment.cache && deployFastCache[cacheKey]) {
|
|
1302
1368
|
log.warn(`⚡ Using cached pkg for ${extension}.`);
|
|
@@ -1305,7 +1371,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1305
1371
|
}
|
|
1306
1372
|
let pkg;
|
|
1307
1373
|
try {
|
|
1308
|
-
pkg = await (0, package_1.packageModule)(
|
|
1374
|
+
pkg = await (0, package_1.packageModule)(modulePath, {
|
|
1309
1375
|
usePlan: opts.deployment.usePlan,
|
|
1310
1376
|
extension: false
|
|
1311
1377
|
});
|
|
@@ -1374,8 +1440,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1374
1440
|
if (name === null) {
|
|
1375
1441
|
throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'deployment' });
|
|
1376
1442
|
}
|
|
1377
|
-
const
|
|
1378
|
-
const modulePath = moduleProject.getModulePath();
|
|
1443
|
+
const modulePath = await this.getEffectiveModulePathFor(name);
|
|
1379
1444
|
if (!modulePath) {
|
|
1380
1445
|
throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
|
|
1381
1446
|
}
|
|
@@ -1442,7 +1507,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1442
1507
|
}
|
|
1443
1508
|
}
|
|
1444
1509
|
else {
|
|
1445
|
-
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
|
|
1510
|
+
const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, (0, path_1.resolve)(this.workspacePath, modules[extension].path), modules, this.workspacePath);
|
|
1446
1511
|
log.info(`📂 Reverting local module: ${extension}`);
|
|
1447
1512
|
try {
|
|
1448
1513
|
const client = new client_1.PgpmMigrate(opts.pg);
|
|
@@ -1475,8 +1540,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1475
1540
|
if (name === null) {
|
|
1476
1541
|
throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'revert' });
|
|
1477
1542
|
}
|
|
1478
|
-
const
|
|
1479
|
-
const modulePath = moduleProject.getModulePath();
|
|
1543
|
+
const modulePath = await this.getEffectiveModulePathFor(name);
|
|
1480
1544
|
if (!modulePath) {
|
|
1481
1545
|
throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
|
|
1482
1546
|
}
|
|
@@ -1503,8 +1567,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1503
1567
|
extensions = await this.resolveWorkspaceExtensionDependencies();
|
|
1504
1568
|
}
|
|
1505
1569
|
else {
|
|
1506
|
-
|
|
1507
|
-
extensions = moduleProject.getModuleExtensions();
|
|
1570
|
+
extensions = this.getModuleExtensionsFor(name);
|
|
1508
1571
|
}
|
|
1509
1572
|
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
1510
1573
|
const targetDescription = name === null ? 'all modules' : name;
|
|
@@ -1517,7 +1580,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1517
1580
|
await pgPool.query(query, [extension]);
|
|
1518
1581
|
}
|
|
1519
1582
|
else {
|
|
1520
|
-
const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
|
|
1583
|
+
const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, (0, path_1.resolve)(this.workspacePath, modules[extension].path), modules, this.workspacePath);
|
|
1521
1584
|
log.info(`📂 Verifying local module: ${extension}`);
|
|
1522
1585
|
try {
|
|
1523
1586
|
const client = new client_1.PgpmMigrate(opts.pg);
|
|
@@ -1549,8 +1612,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
1549
1612
|
if (name === null) {
|
|
1550
1613
|
throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'verification' });
|
|
1551
1614
|
}
|
|
1552
|
-
const
|
|
1553
|
-
const modulePath = moduleProject.getModulePath();
|
|
1615
|
+
const modulePath = await this.getEffectiveModulePathFor(name);
|
|
1554
1616
|
if (!modulePath) {
|
|
1555
1617
|
throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
|
|
1556
1618
|
}
|