@pgpmjs/core 7.15.0 → 7.17.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.js +22 -5
- package/apply/index.d.ts +1 -0
- package/apply/index.js +1 -0
- package/apply/materialize.js +59 -4
- package/apply/profile.d.ts +14 -0
- package/apply/profile.js +38 -0
- package/apply/types.d.ts +17 -69
- package/bundle/artifact.d.ts +2 -2
- package/bundle/artifact.js +2 -2
- package/core/class/pgpm.js +15 -3
- package/esm/apply/apply-spec.js +22 -5
- package/esm/apply/index.js +1 -0
- package/esm/apply/materialize.js +60 -5
- package/esm/apply/profile.js +34 -0
- package/esm/bundle/artifact.js +2 -2
- package/esm/core/class/pgpm.js +15 -3
- package/esm/extensions/compile.js +109 -0
- package/esm/extensions/index.js +4 -0
- package/esm/extensions/manifest.js +139 -0
- package/esm/extensions/resolve-install.js +70 -0
- package/esm/index.js +1 -1
- package/esm/migrate/clean.js +1 -2
- package/esm/projects/deploy.js +19 -4
- package/extensions/compile.d.ts +35 -0
- package/extensions/compile.js +113 -0
- package/extensions/index.d.ts +4 -0
- package/extensions/index.js +20 -0
- package/extensions/manifest.d.ts +78 -0
- package/extensions/manifest.js +144 -0
- package/extensions/resolve-install.d.ts +20 -0
- package/extensions/resolve-install.js +74 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/migrate/clean.js +1 -2
- package/package.json +10 -10
- package/projects/deploy.js +19 -4
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXTENSIONS_MANIFEST_FILES = void 0;
|
|
4
|
+
exports.validateExtensionsManifest = validateExtensionsManifest;
|
|
5
|
+
exports.readExtensionsManifest = readExtensionsManifest;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
/**
|
|
9
|
+
* Declarative extension manifest (`extensions.json`) that lives next to a
|
|
10
|
+
* module's `pgpm.plan` / `.control`. It makes a module self-describing about
|
|
11
|
+
* the PostgreSQL extensions it relates to:
|
|
12
|
+
*
|
|
13
|
+
* - `provides` — the module *is* the extension wrapper: it installs the
|
|
14
|
+
* extension into a chosen schema (with grants). This is what replaces the
|
|
15
|
+
* dynamic `DO $$ EXECUTE 'CREATE EXTENSION ... SCHEMA x' $$` hack, since a
|
|
16
|
+
* literal `CREATE EXTENSION` is stripped at package time.
|
|
17
|
+
* - `consumes` — the module merely *uses* symbols from an extension installed
|
|
18
|
+
* elsewhere (e.g. a bare `crypt()` call) and needs them qualified to wherever
|
|
19
|
+
* the extension actually landed. This is the transform/routing side.
|
|
20
|
+
*
|
|
21
|
+
* `.control` `requires` still names the extension (dependency ordering is
|
|
22
|
+
* unchanged); this manifest only adds the *where + grants* that `.control`
|
|
23
|
+
* cannot express.
|
|
24
|
+
*/
|
|
25
|
+
/** Candidate file names, in precedence order. */
|
|
26
|
+
exports.EXTENSIONS_MANIFEST_FILES = ['pgpm.extensions.json', 'extensions.json'];
|
|
27
|
+
const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
28
|
+
const GRANT_TARGETS = new Set([
|
|
29
|
+
'schema',
|
|
30
|
+
'all-tables',
|
|
31
|
+
'all-sequences',
|
|
32
|
+
'all-functions'
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Validate + normalize a parsed manifest object. Throws with a precise message
|
|
36
|
+
* on malformed input so authors get actionable errors rather than silent
|
|
37
|
+
* misconfiguration.
|
|
38
|
+
*/
|
|
39
|
+
function validateExtensionsManifest(raw, source = 'extensions.json') {
|
|
40
|
+
if (!isPlainObject(raw)) {
|
|
41
|
+
throw new Error(`${source}: manifest must be a JSON object`);
|
|
42
|
+
}
|
|
43
|
+
const manifest = {};
|
|
44
|
+
if (raw.provides !== undefined) {
|
|
45
|
+
if (!isPlainObject(raw.provides)) {
|
|
46
|
+
throw new Error(`${source}: "provides" must be an object keyed by extension name`);
|
|
47
|
+
}
|
|
48
|
+
const provides = {};
|
|
49
|
+
for (const [extname, value] of Object.entries(raw.provides)) {
|
|
50
|
+
provides[extname] = validateProvide(extname, value, source);
|
|
51
|
+
}
|
|
52
|
+
manifest.provides = provides;
|
|
53
|
+
}
|
|
54
|
+
if (raw.consumes !== undefined) {
|
|
55
|
+
if (!isPlainObject(raw.consumes)) {
|
|
56
|
+
throw new Error(`${source}: "consumes" must be an object keyed by extension name`);
|
|
57
|
+
}
|
|
58
|
+
const consumes = {};
|
|
59
|
+
for (const [extname, value] of Object.entries(raw.consumes)) {
|
|
60
|
+
if (!isPlainObject(value)) {
|
|
61
|
+
throw new Error(`${source}: consumes."${extname}" must be an object`);
|
|
62
|
+
}
|
|
63
|
+
const entry = {};
|
|
64
|
+
if (value.symbols !== undefined) {
|
|
65
|
+
if (!Array.isArray(value.symbols) || value.symbols.some((s) => typeof s !== 'string' || !s.trim())) {
|
|
66
|
+
throw new Error(`${source}: consumes."${extname}".symbols must be an array of non-empty strings`);
|
|
67
|
+
}
|
|
68
|
+
entry.symbols = value.symbols;
|
|
69
|
+
}
|
|
70
|
+
consumes[extname] = entry;
|
|
71
|
+
}
|
|
72
|
+
manifest.consumes = consumes;
|
|
73
|
+
}
|
|
74
|
+
return manifest;
|
|
75
|
+
}
|
|
76
|
+
function validateProvide(extname, value, source) {
|
|
77
|
+
if (!isPlainObject(value)) {
|
|
78
|
+
throw new Error(`${source}: provides."${extname}" must be an object`);
|
|
79
|
+
}
|
|
80
|
+
const provide = {};
|
|
81
|
+
if (value.schema !== undefined) {
|
|
82
|
+
if (value.schema !== null && (typeof value.schema !== 'string' || !value.schema.trim())) {
|
|
83
|
+
throw new Error(`${source}: provides."${extname}".schema must be a non-empty string or null`);
|
|
84
|
+
}
|
|
85
|
+
provide.schema = value.schema;
|
|
86
|
+
}
|
|
87
|
+
for (const flag of ['relocatable', 'ifNotExists', 'cascade', 'createSchema', 'dropSchema']) {
|
|
88
|
+
if (value[flag] !== undefined) {
|
|
89
|
+
if (typeof value[flag] !== 'boolean') {
|
|
90
|
+
throw new Error(`${source}: provides."${extname}".${flag} must be a boolean`);
|
|
91
|
+
}
|
|
92
|
+
provide[flag] = value[flag];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (value.grants !== undefined) {
|
|
96
|
+
if (!Array.isArray(value.grants)) {
|
|
97
|
+
throw new Error(`${source}: provides."${extname}".grants must be an array`);
|
|
98
|
+
}
|
|
99
|
+
provide.grants = value.grants.map((g, i) => validateGrant(extname, g, i, source));
|
|
100
|
+
}
|
|
101
|
+
return provide;
|
|
102
|
+
}
|
|
103
|
+
function validateGrant(extname, value, index, source) {
|
|
104
|
+
const where = `provides."${extname}".grants[${index}]`;
|
|
105
|
+
if (!isPlainObject(value)) {
|
|
106
|
+
throw new Error(`${source}: ${where} must be an object`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof value.privileges !== 'string' || !value.privileges.trim()) {
|
|
109
|
+
throw new Error(`${source}: ${where}.privileges must be a non-empty string`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof value.on !== 'string' || !GRANT_TARGETS.has(value.on)) {
|
|
112
|
+
throw new Error(`${source}: ${where}.on must be one of ${[...GRANT_TARGETS].join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
const toValid = (typeof value.to === 'string' && value.to.trim()) ||
|
|
115
|
+
(Array.isArray(value.to) && value.to.length > 0 && value.to.every((r) => typeof r === 'string' && r.trim()));
|
|
116
|
+
if (!toValid) {
|
|
117
|
+
throw new Error(`${source}: ${where}.to must be a role name or non-empty array of role names`);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
privileges: value.privileges,
|
|
121
|
+
on: value.on,
|
|
122
|
+
to: value.to
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Read + validate the `extensions.json` (or `pgpm.extensions.json`) manifest for
|
|
127
|
+
* a module directory. Returns `undefined` when the module declares no manifest.
|
|
128
|
+
*/
|
|
129
|
+
function readExtensionsManifest(moduleDir) {
|
|
130
|
+
for (const file of exports.EXTENSIONS_MANIFEST_FILES) {
|
|
131
|
+
const p = (0, path_1.join)(moduleDir, file);
|
|
132
|
+
if ((0, fs_1.existsSync)(p)) {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse((0, fs_1.readFileSync)(p, 'utf-8'));
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
throw new Error(`Failed to parse ${file} at ${moduleDir}: ${e.message}`);
|
|
139
|
+
}
|
|
140
|
+
return validateExtensionsManifest(parsed, file);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RoleMapping } from '@pgpmjs/types';
|
|
2
|
+
import { CompiledExtensionInstall, CompileExtensionInstallOptions } from './compile';
|
|
3
|
+
/**
|
|
4
|
+
* Build a grant role-name map (workspace portability profile) from the
|
|
5
|
+
* configured {@link RoleMapping}. Keys are the conceptual role names a module
|
|
6
|
+
* author writes in its manifest (`anonymous`/`authenticated`/`administrator`);
|
|
7
|
+
* values are the actual role names for this database.
|
|
8
|
+
*/
|
|
9
|
+
export declare function roleMapFromRoles(roles?: RoleMapping): Record<string, string> | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Find a declarative install for an external extension by scanning the resolved
|
|
12
|
+
* local modules for an `extensions.json` that `provides` it, and compiling it.
|
|
13
|
+
*
|
|
14
|
+
* Returns `undefined` when no module declares the extension — the caller then
|
|
15
|
+
* falls back to the legacy `CREATE EXTENSION ... CASCADE` (no schema, no grants).
|
|
16
|
+
* The first declaring module wins; a later contradictory declaration throws.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findExtensionInstall(extname: string, localModules: string[], modules: Record<string, {
|
|
19
|
+
path: string;
|
|
20
|
+
}>, workspacePath: string, options?: CompileExtensionInstallOptions): CompiledExtensionInstall | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.roleMapFromRoles = roleMapFromRoles;
|
|
4
|
+
exports.findExtensionInstall = findExtensionInstall;
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const compile_1 = require("./compile");
|
|
7
|
+
const manifest_1 = require("./manifest");
|
|
8
|
+
/**
|
|
9
|
+
* Build a grant role-name map (workspace portability profile) from the
|
|
10
|
+
* configured {@link RoleMapping}. Keys are the conceptual role names a module
|
|
11
|
+
* author writes in its manifest (`anonymous`/`authenticated`/`administrator`);
|
|
12
|
+
* values are the actual role names for this database.
|
|
13
|
+
*/
|
|
14
|
+
function roleMapFromRoles(roles) {
|
|
15
|
+
if (!roles)
|
|
16
|
+
return undefined;
|
|
17
|
+
const map = {};
|
|
18
|
+
for (const key of ['anonymous', 'authenticated', 'administrator', 'authenticatedClient']) {
|
|
19
|
+
const value = roles[key];
|
|
20
|
+
if (value)
|
|
21
|
+
map[key] = value;
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(map).length > 0 ? map : undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Find a declarative install for an external extension by scanning the resolved
|
|
27
|
+
* local modules for an `extensions.json` that `provides` it, and compiling it.
|
|
28
|
+
*
|
|
29
|
+
* Returns `undefined` when no module declares the extension — the caller then
|
|
30
|
+
* falls back to the legacy `CREATE EXTENSION ... CASCADE` (no schema, no grants).
|
|
31
|
+
* The first declaring module wins; a later contradictory declaration throws.
|
|
32
|
+
*/
|
|
33
|
+
function findExtensionInstall(extname, localModules, modules, workspacePath, options = {}) {
|
|
34
|
+
let winner;
|
|
35
|
+
for (const moduleName of localModules) {
|
|
36
|
+
const mod = modules[moduleName];
|
|
37
|
+
if (!mod)
|
|
38
|
+
continue;
|
|
39
|
+
const manifest = (0, manifest_1.readExtensionsManifest)((0, path_1.resolve)(workspacePath, mod.path));
|
|
40
|
+
const provide = manifest?.provides?.[extname];
|
|
41
|
+
if (!provide)
|
|
42
|
+
continue;
|
|
43
|
+
if (winner && !sameProvide(winner.provide, provide)) {
|
|
44
|
+
throw new Error(`Conflicting extensions.json "provides" for "${extname}": ` +
|
|
45
|
+
`module "${winner.moduleName}" and module "${moduleName}" declare incompatible ` +
|
|
46
|
+
`install schema/grants. Exactly one provider (or identical declarations) is required.`);
|
|
47
|
+
}
|
|
48
|
+
if (!winner)
|
|
49
|
+
winner = { moduleName, provide };
|
|
50
|
+
}
|
|
51
|
+
return winner ? (0, compile_1.compileExtensionInstall)(extname, winner.provide, options) : undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Structural equality of two provide declarations (order-insensitive grants). */
|
|
54
|
+
function sameProvide(a, b) {
|
|
55
|
+
return normalizeProvide(a) === normalizeProvide(b);
|
|
56
|
+
}
|
|
57
|
+
function normalizeProvide(p) {
|
|
58
|
+
const grants = (p.grants ?? [])
|
|
59
|
+
.map(g => ({
|
|
60
|
+
privileges: g.privileges,
|
|
61
|
+
on: g.on,
|
|
62
|
+
to: (Array.isArray(g.to) ? [...g.to] : [g.to]).sort()
|
|
63
|
+
}))
|
|
64
|
+
.sort((x, y) => JSON.stringify(x).localeCompare(JSON.stringify(y)));
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
schema: p.schema ?? null,
|
|
67
|
+
relocatable: p.relocatable ?? null,
|
|
68
|
+
ifNotExists: p.ifNotExists ?? null,
|
|
69
|
+
cascade: p.cascade ?? null,
|
|
70
|
+
createSchema: p.createSchema ?? null,
|
|
71
|
+
dropSchema: p.dropSchema ?? null,
|
|
72
|
+
grants
|
|
73
|
+
});
|
|
74
|
+
}
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.withTransaction = exports.executeQuery = exports.hashString = exports.hashFile = exports.parseAuthor = exports.PgpmInit = exports.PgpmMigrate = exports.cleanSql = void 0;
|
|
18
18
|
__exportStar(require("./core/class/pgpm"), exports);
|
|
19
19
|
__exportStar(require("./rebundle"), exports);
|
|
20
|
-
__exportStar(require("./extensions
|
|
20
|
+
__exportStar(require("./extensions"), exports);
|
|
21
21
|
__exportStar(require("./modules/modules"), exports);
|
|
22
22
|
__exportStar(require("./packaging/package"), exports);
|
|
23
23
|
__exportStar(require("./packaging/sync-versions"), exports);
|
package/migrate/clean.js
CHANGED
|
@@ -5,8 +5,7 @@ const pgsql_parser_1 = require("pgsql-parser");
|
|
|
5
5
|
const filterStatements = (stmts) => {
|
|
6
6
|
const filteredStmts = stmts.filter(node => {
|
|
7
7
|
const stmt = node.stmt;
|
|
8
|
-
return stmt && !stmt.hasOwnProperty('TransactionStmt')
|
|
9
|
-
!stmt.hasOwnProperty('CreateExtensionStmt');
|
|
8
|
+
return stmt && !stmt.hasOwnProperty('TransactionStmt');
|
|
10
9
|
});
|
|
11
10
|
const hasFiltered = filteredStmts.length !== stmts.length;
|
|
12
11
|
return { filteredStmts, hasFiltered };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.17.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM Package and Migration Tools",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,24 +48,24 @@
|
|
|
48
48
|
"makage": "^0.3.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@pgpmjs/ast": "^0.9.
|
|
52
|
-
"@pgpmjs/bundle": "^0.12.
|
|
53
|
-
"@pgpmjs/env": "^2.
|
|
51
|
+
"@pgpmjs/ast": "^0.9.4",
|
|
52
|
+
"@pgpmjs/bundle": "^0.12.2",
|
|
53
|
+
"@pgpmjs/env": "^2.39.1",
|
|
54
54
|
"@pgpmjs/logger": "^2.22.0",
|
|
55
|
-
"@pgpmjs/server-utils": "^3.23.
|
|
56
|
-
"@pgpmjs/slice": "^0.
|
|
57
|
-
"@pgpmjs/transform": "^0.
|
|
58
|
-
"@pgpmjs/types": "^2.
|
|
55
|
+
"@pgpmjs/server-utils": "^3.23.3",
|
|
56
|
+
"@pgpmjs/slice": "^0.11.0",
|
|
57
|
+
"@pgpmjs/transform": "^0.12.0",
|
|
58
|
+
"@pgpmjs/types": "^2.48.0",
|
|
59
59
|
"csv-to-pg": "^4.8.0",
|
|
60
60
|
"genomic": "^5.6.2",
|
|
61
61
|
"glob": "^13.0.6",
|
|
62
62
|
"parse-package-name": "^1.0.0",
|
|
63
63
|
"pg": "^8.21.0",
|
|
64
|
-
"pg-cache": "^3.23.
|
|
64
|
+
"pg-cache": "^3.23.3",
|
|
65
65
|
"pg-env": "^1.26.0",
|
|
66
66
|
"pgsql-deparser": "^18.1.1",
|
|
67
67
|
"pgsql-parser": "^18.1.1",
|
|
68
68
|
"yanse": "^0.2.1"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "2832d5632d11eb22e1437df773706ac6a99175fe"
|
|
71
71
|
}
|
package/projects/deploy.js
CHANGED
|
@@ -44,6 +44,7 @@ const materialize_1 = require("../apply/materialize");
|
|
|
44
44
|
const apply_spec_1 = require("../apply/apply-spec");
|
|
45
45
|
const deploy_bundled_1 = require("../bundle/deploy-bundled");
|
|
46
46
|
const pgpm_1 = require("../core/class/pgpm");
|
|
47
|
+
const extensions_1 = require("../extensions");
|
|
47
48
|
const client_1 = require("../migrate/client");
|
|
48
49
|
const deps_1 = require("../resolution/deps");
|
|
49
50
|
const log = new logger_1.Logger('deploy');
|
|
@@ -63,14 +64,28 @@ const deployProject = async (opts, name, database, pkg, toChange) => {
|
|
|
63
64
|
? (0, deps_1.resolveExtensionDependencies)(name, modules)
|
|
64
65
|
: new pgpm_1.PgpmPackage(modulePath, { extensionsDir: pkg.extensionsDir }).getModuleExtensions();
|
|
65
66
|
const pgPool = (0, pg_cache_1.getPgPool)({ ...opts.pg, database });
|
|
67
|
+
// Local modules (in resolve order) that may carry an `extensions.json`
|
|
68
|
+
// declaring how an external extension should be installed (schema + grants).
|
|
69
|
+
const localModules = extensions.resolved.filter((m) => !extensions.external.includes(m));
|
|
70
|
+
const roleMap = (0, extensions_1.roleMapFromRoles)(mergedOpts.db?.roles);
|
|
66
71
|
log.success(`🚀 Starting deployment to database ${database}...`);
|
|
67
72
|
for (const extension of extensions.resolved) {
|
|
68
73
|
try {
|
|
69
74
|
if (extensions.external.includes(extension)) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const install = (0, extensions_1.findExtensionInstall)(extension, localModules, modules, pkg.workspacePath, { roleMap });
|
|
76
|
+
if (install) {
|
|
77
|
+
log.info(`📥 Installing external extension (declarative): ${extension}`);
|
|
78
|
+
for (const stmt of install.deploy) {
|
|
79
|
+
log.debug(`> ${stmt}`);
|
|
80
|
+
await pgPool.query(stmt);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const msg = `CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;`;
|
|
85
|
+
log.info(`📥 Installing external extension: ${extension}`);
|
|
86
|
+
log.debug(`> ${msg}`);
|
|
87
|
+
await pgPool.query(msg);
|
|
88
|
+
}
|
|
74
89
|
}
|
|
75
90
|
else {
|
|
76
91
|
const sourcePath = (0, path_1.resolve)(pkg.workspacePath, modules[extension].path);
|