@pgpmjs/core 4.1.2 → 4.2.1
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/core/class/pgpm.d.ts +10 -0
- package/core/class/pgpm.js +44 -2
- package/esm/core/class/pgpm.js +44 -2
- package/esm/export/export-migrations.js +167 -45
- package/esm/files/plan/writer.js +6 -2
- package/esm/files/sql/writer.js +9 -5
- package/esm/modules/modules.js +39 -0
- package/export/export-migrations.d.ts +12 -1
- package/export/export-migrations.js +166 -44
- package/files/plan/writer.d.ts +7 -3
- package/files/plan/writer.js +8 -3
- package/files/sql/writer.d.ts +7 -3
- package/files/sql/writer.js +11 -7
- package/files/types/index.d.ts +5 -1
- package/modules/modules.d.ts +22 -0
- package/modules/modules.js +41 -1
- package/package.json +2 -2
package/core/class/pgpm.d.ts
CHANGED
|
@@ -145,6 +145,16 @@ export declare class PgpmPackage {
|
|
|
145
145
|
installed: string[];
|
|
146
146
|
installedVersions: Record<string, string>;
|
|
147
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* Returns all pgpm modules installed in the workspace extensions directory.
|
|
150
|
+
* Unlike getInstalledModules(), this does NOT require being inside a module.
|
|
151
|
+
* It scans the workspace/extensions/ directory directly to find installed packages.
|
|
152
|
+
*
|
|
153
|
+
* This is useful for checking what's available at workspace level before a module exists.
|
|
154
|
+
*
|
|
155
|
+
* @returns Array of installed npm package names (e.g., '@pgpm/base32', '@pgpm/totp')
|
|
156
|
+
*/
|
|
157
|
+
getWorkspaceInstalledModules(): string[];
|
|
148
158
|
/**
|
|
149
159
|
* Updates installed pgpm modules to their latest versions from npm.
|
|
150
160
|
* Re-installs each module to get the latest version.
|
package/core/class/pgpm.js
CHANGED
|
@@ -62,6 +62,11 @@ const package_1 = require("../../packaging/package");
|
|
|
62
62
|
const deps_1 = require("../../resolution/deps");
|
|
63
63
|
const target_utils_1 = require("../../utils/target-utils");
|
|
64
64
|
const logger = new logger_1.Logger('pgpm');
|
|
65
|
+
/**
|
|
66
|
+
* Directory name for workspace extensions.
|
|
67
|
+
* Extensions are installed globally in the workspace's extensions/ directory.
|
|
68
|
+
*/
|
|
69
|
+
const EXTENSIONS_DIR = 'extensions';
|
|
65
70
|
function getUTCTimestamp(d = new Date()) {
|
|
66
71
|
return (d.getUTCFullYear() +
|
|
67
72
|
'-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
|
|
@@ -786,7 +791,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
786
791
|
this.ensureWorkspace();
|
|
787
792
|
this.ensureModule();
|
|
788
793
|
const originalDir = process.cwd();
|
|
789
|
-
const skitchExtDir = path_1.default.join(this.workspacePath,
|
|
794
|
+
const skitchExtDir = path_1.default.join(this.workspacePath, EXTENSIONS_DIR);
|
|
790
795
|
const pkgJsonPath = path_1.default.join(this.modulePath, 'package.json');
|
|
791
796
|
if (!fs_1.default.existsSync(pkgJsonPath)) {
|
|
792
797
|
throw new Error(`No package.json found at module path: ${this.modulePath}`);
|
|
@@ -889,7 +894,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
889
894
|
}
|
|
890
895
|
const pkgData = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
891
896
|
const dependencies = pkgData.dependencies || {};
|
|
892
|
-
const skitchExtDir = path_1.default.join(this.workspacePath,
|
|
897
|
+
const skitchExtDir = path_1.default.join(this.workspacePath, EXTENSIONS_DIR);
|
|
893
898
|
const installed = [];
|
|
894
899
|
const installedVersions = {};
|
|
895
900
|
for (const [name, version] of Object.entries(dependencies)) {
|
|
@@ -901,6 +906,43 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
901
906
|
}
|
|
902
907
|
return { installed, installedVersions };
|
|
903
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Returns all pgpm modules installed in the workspace extensions directory.
|
|
911
|
+
* Unlike getInstalledModules(), this does NOT require being inside a module.
|
|
912
|
+
* It scans the workspace/extensions/ directory directly to find installed packages.
|
|
913
|
+
*
|
|
914
|
+
* This is useful for checking what's available at workspace level before a module exists.
|
|
915
|
+
*
|
|
916
|
+
* @returns Array of installed npm package names (e.g., '@pgpm/base32', '@pgpm/totp')
|
|
917
|
+
*/
|
|
918
|
+
getWorkspaceInstalledModules() {
|
|
919
|
+
this.ensureWorkspace();
|
|
920
|
+
const extensionsDir = path_1.default.join(this.workspacePath, EXTENSIONS_DIR);
|
|
921
|
+
if (!fs_1.default.existsSync(extensionsDir)) {
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
const installed = [];
|
|
925
|
+
const entries = fs_1.default.readdirSync(extensionsDir, { withFileTypes: true });
|
|
926
|
+
for (const entry of entries) {
|
|
927
|
+
if (!entry.isDirectory())
|
|
928
|
+
continue;
|
|
929
|
+
if (entry.name.startsWith('@')) {
|
|
930
|
+
// Handle scoped packages like @pgpm/base32
|
|
931
|
+
const scopeDir = path_1.default.join(extensionsDir, entry.name);
|
|
932
|
+
const scopedEntries = fs_1.default.readdirSync(scopeDir, { withFileTypes: true });
|
|
933
|
+
for (const scopedEntry of scopedEntries) {
|
|
934
|
+
if (scopedEntry.isDirectory()) {
|
|
935
|
+
installed.push(`${entry.name}/${scopedEntry.name}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
// Handle non-scoped packages
|
|
941
|
+
installed.push(entry.name);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return installed;
|
|
945
|
+
}
|
|
904
946
|
/**
|
|
905
947
|
* Updates installed pgpm modules to their latest versions from npm.
|
|
906
948
|
* Re-installs each module to get the latest version.
|
package/esm/core/class/pgpm.js
CHANGED
|
@@ -23,6 +23,11 @@ import { packageModule } from '../../packaging/package';
|
|
|
23
23
|
import { resolveExtensionDependencies, resolveDependencies } from '../../resolution/deps';
|
|
24
24
|
import { parseTarget } from '../../utils/target-utils';
|
|
25
25
|
const logger = new Logger('pgpm');
|
|
26
|
+
/**
|
|
27
|
+
* Directory name for workspace extensions.
|
|
28
|
+
* Extensions are installed globally in the workspace's extensions/ directory.
|
|
29
|
+
*/
|
|
30
|
+
const EXTENSIONS_DIR = 'extensions';
|
|
26
31
|
function getUTCTimestamp(d = new Date()) {
|
|
27
32
|
return (d.getUTCFullYear() +
|
|
28
33
|
'-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
|
|
@@ -747,7 +752,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
747
752
|
this.ensureWorkspace();
|
|
748
753
|
this.ensureModule();
|
|
749
754
|
const originalDir = process.cwd();
|
|
750
|
-
const skitchExtDir = path.join(this.workspacePath,
|
|
755
|
+
const skitchExtDir = path.join(this.workspacePath, EXTENSIONS_DIR);
|
|
751
756
|
const pkgJsonPath = path.join(this.modulePath, 'package.json');
|
|
752
757
|
if (!fs.existsSync(pkgJsonPath)) {
|
|
753
758
|
throw new Error(`No package.json found at module path: ${this.modulePath}`);
|
|
@@ -850,7 +855,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
850
855
|
}
|
|
851
856
|
const pkgData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
852
857
|
const dependencies = pkgData.dependencies || {};
|
|
853
|
-
const skitchExtDir = path.join(this.workspacePath,
|
|
858
|
+
const skitchExtDir = path.join(this.workspacePath, EXTENSIONS_DIR);
|
|
854
859
|
const installed = [];
|
|
855
860
|
const installedVersions = {};
|
|
856
861
|
for (const [name, version] of Object.entries(dependencies)) {
|
|
@@ -862,6 +867,43 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
|
|
|
862
867
|
}
|
|
863
868
|
return { installed, installedVersions };
|
|
864
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* Returns all pgpm modules installed in the workspace extensions directory.
|
|
872
|
+
* Unlike getInstalledModules(), this does NOT require being inside a module.
|
|
873
|
+
* It scans the workspace/extensions/ directory directly to find installed packages.
|
|
874
|
+
*
|
|
875
|
+
* This is useful for checking what's available at workspace level before a module exists.
|
|
876
|
+
*
|
|
877
|
+
* @returns Array of installed npm package names (e.g., '@pgpm/base32', '@pgpm/totp')
|
|
878
|
+
*/
|
|
879
|
+
getWorkspaceInstalledModules() {
|
|
880
|
+
this.ensureWorkspace();
|
|
881
|
+
const extensionsDir = path.join(this.workspacePath, EXTENSIONS_DIR);
|
|
882
|
+
if (!fs.existsSync(extensionsDir)) {
|
|
883
|
+
return [];
|
|
884
|
+
}
|
|
885
|
+
const installed = [];
|
|
886
|
+
const entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
|
|
887
|
+
for (const entry of entries) {
|
|
888
|
+
if (!entry.isDirectory())
|
|
889
|
+
continue;
|
|
890
|
+
if (entry.name.startsWith('@')) {
|
|
891
|
+
// Handle scoped packages like @pgpm/base32
|
|
892
|
+
const scopeDir = path.join(extensionsDir, entry.name);
|
|
893
|
+
const scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
894
|
+
for (const scopedEntry of scopedEntries) {
|
|
895
|
+
if (scopedEntry.isDirectory()) {
|
|
896
|
+
installed.push(`${entry.name}/${scopedEntry.name}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
// Handle non-scoped packages
|
|
902
|
+
installed.push(entry.name);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return installed;
|
|
906
|
+
}
|
|
865
907
|
/**
|
|
866
908
|
* Updates installed pgpm modules to their latest versions from npm.
|
|
867
909
|
* Re-installs each module to get the latest version.
|
|
@@ -3,9 +3,95 @@ import { sync as glob } from 'glob';
|
|
|
3
3
|
import { toSnakeCase } from 'komoji';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { getPgPool } from 'pg-cache';
|
|
6
|
-
import {
|
|
6
|
+
import { PgpmPackage } from '../core/class/pgpm';
|
|
7
|
+
import { writePgpmFiles, writePgpmPlan } from '../files';
|
|
8
|
+
import { getMissingInstallableModules } from '../modules/modules';
|
|
7
9
|
import { exportMeta } from './export-meta';
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Required extensions for database schema exports.
|
|
12
|
+
* Includes native PostgreSQL extensions and pgpm modules.
|
|
13
|
+
*/
|
|
14
|
+
const DB_REQUIRED_EXTENSIONS = [
|
|
15
|
+
'plpgsql',
|
|
16
|
+
'uuid-ossp',
|
|
17
|
+
'citext',
|
|
18
|
+
'pgcrypto',
|
|
19
|
+
'btree_gist',
|
|
20
|
+
'postgis',
|
|
21
|
+
'hstore',
|
|
22
|
+
'db-meta-schema',
|
|
23
|
+
'pgpm-inflection',
|
|
24
|
+
'pgpm-uuid',
|
|
25
|
+
'pgpm-utils',
|
|
26
|
+
'pgpm-database-jobs',
|
|
27
|
+
'pgpm-jwt-claims',
|
|
28
|
+
'pgpm-stamps',
|
|
29
|
+
'pgpm-base32',
|
|
30
|
+
'pgpm-totp',
|
|
31
|
+
'pgpm-types'
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Required extensions for service/meta exports.
|
|
35
|
+
* Includes native PostgreSQL extensions and pgpm modules for metadata management.
|
|
36
|
+
*/
|
|
37
|
+
const SERVICE_REQUIRED_EXTENSIONS = [
|
|
38
|
+
'plpgsql',
|
|
39
|
+
'db-meta-schema',
|
|
40
|
+
'db-meta-modules'
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Checks which pgpm modules from the extensions list are missing from the workspace
|
|
44
|
+
* and prompts the user if they want to install them.
|
|
45
|
+
*
|
|
46
|
+
* This function only does detection and prompting - it does NOT install.
|
|
47
|
+
* Use installMissingModules() after the module is created to do the actual installation.
|
|
48
|
+
*
|
|
49
|
+
* @param project - The PgpmPackage instance (only needs workspace context)
|
|
50
|
+
* @param extensions - List of extension names (control file names)
|
|
51
|
+
* @param prompter - Optional prompter for interactive confirmation
|
|
52
|
+
* @returns Object with missing modules and whether user wants to install them
|
|
53
|
+
*/
|
|
54
|
+
const detectMissingModules = async (project, extensions, prompter) => {
|
|
55
|
+
// Use workspace-level check - doesn't require being inside a module
|
|
56
|
+
const installed = project.getWorkspaceInstalledModules();
|
|
57
|
+
const missingModules = getMissingInstallableModules(extensions, installed);
|
|
58
|
+
if (missingModules.length === 0) {
|
|
59
|
+
return { missingModules: [], shouldInstall: false };
|
|
60
|
+
}
|
|
61
|
+
const missingNames = missingModules.map(m => m.npmName);
|
|
62
|
+
console.log(`\nMissing pgpm modules detected: ${missingNames.join(', ')}`);
|
|
63
|
+
if (prompter) {
|
|
64
|
+
const { install } = await prompter.prompt({}, [
|
|
65
|
+
{
|
|
66
|
+
type: 'confirm',
|
|
67
|
+
name: 'install',
|
|
68
|
+
message: `Install missing modules (${missingNames.join(', ')})?`,
|
|
69
|
+
default: true
|
|
70
|
+
}
|
|
71
|
+
]);
|
|
72
|
+
return { missingModules, shouldInstall: install };
|
|
73
|
+
}
|
|
74
|
+
return { missingModules, shouldInstall: false };
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Installs missing modules into a specific module directory.
|
|
78
|
+
* Must be called after the module has been created.
|
|
79
|
+
*
|
|
80
|
+
* @param moduleDir - The directory of the module to install into
|
|
81
|
+
* @param missingModules - Array of missing modules to install
|
|
82
|
+
*/
|
|
83
|
+
const installMissingModules = async (moduleDir, missingModules) => {
|
|
84
|
+
if (missingModules.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const missingNames = missingModules.map(m => m.npmName);
|
|
88
|
+
console.log('Installing missing modules...');
|
|
89
|
+
// Create a new PgpmPackage instance pointing to the module directory
|
|
90
|
+
const moduleProject = new PgpmPackage(moduleDir);
|
|
91
|
+
await moduleProject.installModules(...missingNames);
|
|
92
|
+
console.log('Modules installed successfully.');
|
|
93
|
+
};
|
|
94
|
+
const exportMigrationsToDisk = async ({ project, options, database, databaseId, databaseName, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }) => {
|
|
9
95
|
outdir = outdir + '/';
|
|
10
96
|
const pgPool = getPgPool({
|
|
11
97
|
...options.pg,
|
|
@@ -33,47 +119,51 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
|
|
|
33
119
|
outdir,
|
|
34
120
|
author
|
|
35
121
|
};
|
|
122
|
+
// Build description for the database extension package
|
|
123
|
+
const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
|
|
36
124
|
if (results?.rows?.length > 0) {
|
|
37
|
-
|
|
125
|
+
// Detect missing modules at workspace level and prompt user
|
|
126
|
+
const dbMissingResult = await detectMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter);
|
|
127
|
+
// Create/prepare the module directory
|
|
128
|
+
const dbModuleDir = await preparePackage({
|
|
38
129
|
project,
|
|
39
130
|
author,
|
|
40
131
|
outdir,
|
|
41
132
|
name,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'citext',
|
|
46
|
-
'pgcrypto',
|
|
47
|
-
'btree_gist',
|
|
48
|
-
'postgis',
|
|
49
|
-
'hstore',
|
|
50
|
-
'db-meta-schema',
|
|
51
|
-
'pgpm-inflection',
|
|
52
|
-
'pgpm-uuid',
|
|
53
|
-
'pgpm-utils',
|
|
54
|
-
'pgpm-database-jobs',
|
|
55
|
-
'pgpm-jwt-claims',
|
|
56
|
-
'pgpm-stamps',
|
|
57
|
-
'pgpm-base32',
|
|
58
|
-
'pgpm-totp',
|
|
59
|
-
'pgpm-types'
|
|
60
|
-
]
|
|
133
|
+
description: dbExtensionDesc,
|
|
134
|
+
extensions: [...DB_REQUIRED_EXTENSIONS],
|
|
135
|
+
prompter
|
|
61
136
|
});
|
|
62
|
-
|
|
63
|
-
|
|
137
|
+
// Install missing modules if user confirmed (now that module exists)
|
|
138
|
+
if (dbMissingResult.shouldInstall) {
|
|
139
|
+
await installMissingModules(dbModuleDir, dbMissingResult.missingModules);
|
|
140
|
+
}
|
|
141
|
+
writePgpmPlan(results.rows, opts);
|
|
142
|
+
writePgpmFiles(results.rows, opts);
|
|
64
143
|
let meta = await exportMeta({
|
|
65
144
|
opts: options,
|
|
66
145
|
dbname: database,
|
|
67
146
|
database_id: databaseId
|
|
68
147
|
});
|
|
69
148
|
meta = replacer(meta);
|
|
70
|
-
|
|
149
|
+
// Build description for the meta/service extension package
|
|
150
|
+
const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
|
|
151
|
+
// Detect missing modules at workspace level and prompt user
|
|
152
|
+
const svcMissingResult = await detectMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter);
|
|
153
|
+
// Create/prepare the module directory
|
|
154
|
+
const svcModuleDir = await preparePackage({
|
|
71
155
|
project,
|
|
72
156
|
author,
|
|
73
157
|
outdir,
|
|
74
|
-
|
|
75
|
-
|
|
158
|
+
name: metaExtensionName,
|
|
159
|
+
description: metaDesc,
|
|
160
|
+
extensions: [...SERVICE_REQUIRED_EXTENSIONS],
|
|
161
|
+
prompter
|
|
76
162
|
});
|
|
163
|
+
// Install missing modules if user confirmed (now that module exists)
|
|
164
|
+
if (svcMissingResult.shouldInstall) {
|
|
165
|
+
await installMissingModules(svcModuleDir, svcMissingResult.missingModules);
|
|
166
|
+
}
|
|
77
167
|
const metaReplacer = makeReplacer({
|
|
78
168
|
schemas: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name)),
|
|
79
169
|
name: metaExtensionName
|
|
@@ -100,11 +190,15 @@ $LQLMIGRATION$;
|
|
|
100
190
|
|
|
101
191
|
${meta}
|
|
102
192
|
|
|
103
|
-
UPDATE
|
|
104
|
-
|
|
193
|
+
-- TODO: Research needed - These UPDATE statements may be a security leak.
|
|
194
|
+
-- They appear to rebind exported metadata to the target database after import,
|
|
195
|
+
-- but exposing dbname in meta_public tables could leak internal database names.
|
|
196
|
+
-- Consider removing entirely or gating behind an explicit flag.
|
|
197
|
+
-- UPDATE meta_public.apis
|
|
198
|
+
-- SET dbname = current_database() WHERE TRUE;
|
|
105
199
|
|
|
106
|
-
UPDATE meta_public.sites
|
|
107
|
-
|
|
200
|
+
-- UPDATE meta_public.sites
|
|
201
|
+
-- SET dbname = current_database() WHERE TRUE;
|
|
108
202
|
|
|
109
203
|
SET session_replication_role TO DEFAULT;
|
|
110
204
|
`
|
|
@@ -112,50 +206,78 @@ SET session_replication_role TO DEFAULT;
|
|
|
112
206
|
];
|
|
113
207
|
opts.replacer = metaReplacer.replacer;
|
|
114
208
|
opts.name = metaExtensionName;
|
|
115
|
-
|
|
116
|
-
|
|
209
|
+
writePgpmPlan(metaPackage, opts);
|
|
210
|
+
writePgpmFiles(metaPackage, opts);
|
|
117
211
|
}
|
|
118
212
|
pgPool.end();
|
|
119
213
|
};
|
|
120
|
-
export const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, metaExtensionName }) => {
|
|
214
|
+
export const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }) => {
|
|
121
215
|
for (let v = 0; v < dbInfo.database_ids.length; v++) {
|
|
122
216
|
const databaseId = dbInfo.database_ids[v];
|
|
123
217
|
await exportMigrationsToDisk({
|
|
124
218
|
project,
|
|
125
219
|
options,
|
|
126
220
|
extensionName,
|
|
221
|
+
extensionDesc,
|
|
127
222
|
metaExtensionName,
|
|
223
|
+
metaExtensionDesc,
|
|
128
224
|
database: dbInfo.dbname,
|
|
225
|
+
databaseName: dbInfo.databaseName,
|
|
129
226
|
databaseId,
|
|
130
227
|
schema_names,
|
|
131
228
|
author,
|
|
132
|
-
outdir
|
|
229
|
+
outdir,
|
|
230
|
+
prompter
|
|
133
231
|
});
|
|
134
232
|
}
|
|
135
233
|
};
|
|
136
234
|
/**
|
|
137
|
-
* Creates a
|
|
235
|
+
* Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
|
|
236
|
+
* If the module already exists and a prompter is provided, prompts the user for confirmation.
|
|
237
|
+
*
|
|
238
|
+
* @returns The absolute path to the created/prepared module directory
|
|
138
239
|
*/
|
|
139
|
-
const preparePackage = async ({ project, author, outdir, name, extensions }) => {
|
|
240
|
+
const preparePackage = async ({ project, author, outdir, name, description, extensions, prompter }) => {
|
|
140
241
|
const curDir = process.cwd();
|
|
141
|
-
const
|
|
142
|
-
mkdirSync(
|
|
143
|
-
process.chdir(
|
|
144
|
-
const plan = glob(path.join(
|
|
242
|
+
const pgpmDir = path.resolve(path.join(outdir, name));
|
|
243
|
+
mkdirSync(pgpmDir, { recursive: true });
|
|
244
|
+
process.chdir(pgpmDir);
|
|
245
|
+
const plan = glob(path.join(pgpmDir, 'pgpm.plan'));
|
|
145
246
|
if (!plan.length) {
|
|
146
247
|
await project.initModule({
|
|
147
248
|
name,
|
|
148
|
-
description
|
|
249
|
+
description,
|
|
149
250
|
author,
|
|
150
251
|
extensions,
|
|
252
|
+
answers: {
|
|
253
|
+
moduleName: name,
|
|
254
|
+
moduleDesc: description,
|
|
255
|
+
access: 'restricted',
|
|
256
|
+
license: 'CLOSED'
|
|
257
|
+
}
|
|
151
258
|
});
|
|
152
259
|
}
|
|
153
260
|
else {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
261
|
+
if (prompter) {
|
|
262
|
+
const { overwrite } = await prompter.prompt({}, [
|
|
263
|
+
{
|
|
264
|
+
type: 'confirm',
|
|
265
|
+
name: 'overwrite',
|
|
266
|
+
message: `Module "${name}" already exists at ${pgpmDir}. Overwrite deploy/revert/verify directories?`,
|
|
267
|
+
default: false
|
|
268
|
+
}
|
|
269
|
+
]);
|
|
270
|
+
if (!overwrite) {
|
|
271
|
+
process.chdir(curDir);
|
|
272
|
+
throw new Error(`Export cancelled: Module "${name}" already exists.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
rmSync(path.resolve(pgpmDir, 'deploy'), { recursive: true, force: true });
|
|
276
|
+
rmSync(path.resolve(pgpmDir, 'revert'), { recursive: true, force: true });
|
|
277
|
+
rmSync(path.resolve(pgpmDir, 'verify'), { recursive: true, force: true });
|
|
157
278
|
}
|
|
158
279
|
process.chdir(curDir);
|
|
280
|
+
return pgpmDir;
|
|
159
281
|
};
|
|
160
282
|
/**
|
|
161
283
|
* Generates a function for replacing schema names and extension names in strings.
|
package/esm/files/plan/writer.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
/**
|
|
4
|
-
* Write a
|
|
4
|
+
* Write a PGPM plan file based on the provided rows
|
|
5
5
|
*/
|
|
6
|
-
export function
|
|
6
|
+
export function writePgpmPlan(rows, opts) {
|
|
7
7
|
const dir = path.resolve(path.join(opts.outdir, opts.name));
|
|
8
8
|
fs.mkdirSync(dir, { recursive: true });
|
|
9
9
|
const date = () => '2017-08-11T08:11:51Z'; // stubbed timestamp
|
|
@@ -126,3 +126,7 @@ export function generateTagLineContent(tag) {
|
|
|
126
126
|
}
|
|
127
127
|
return line;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* @deprecated Use writePgpmPlan instead. This alias is kept for backwards compatibility.
|
|
131
|
+
*/
|
|
132
|
+
export const writeSqitchPlan = writePgpmPlan;
|
package/esm/files/sql/writer.js
CHANGED
|
@@ -2,9 +2,9 @@ import { getEnvOptions } from '@pgpmjs/env';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
/**
|
|
5
|
-
* Write SQL files for
|
|
5
|
+
* Write SQL files for PGPM migrations (deploy, revert, verify)
|
|
6
6
|
*/
|
|
7
|
-
export const
|
|
7
|
+
export const writePgpmFiles = (rows, opts) => {
|
|
8
8
|
rows.forEach((row) => writeVerify(row, opts));
|
|
9
9
|
rows.forEach((row) => writeRevert(row, opts));
|
|
10
10
|
rows.forEach((row) => writeDeploy(row, opts));
|
|
@@ -18,7 +18,7 @@ const ordered = (arr) => {
|
|
|
18
18
|
return arr.sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* Write a deploy SQL file for a
|
|
21
|
+
* Write a deploy SQL file for a PGPM change
|
|
22
22
|
*/
|
|
23
23
|
const writeDeploy = (row, opts) => {
|
|
24
24
|
const globalOpts = getEnvOptions({
|
|
@@ -50,7 +50,7 @@ ${useTx ? 'COMMIT;' : ''}
|
|
|
50
50
|
fs.writeFileSync(actualFile, content);
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
|
-
* Write a verify SQL file for a
|
|
53
|
+
* Write a verify SQL file for a PGPM change
|
|
54
54
|
*/
|
|
55
55
|
const writeVerify = (row, opts) => {
|
|
56
56
|
const globalOpts = getEnvOptions({
|
|
@@ -78,7 +78,7 @@ ${useTx ? 'COMMIT;' : ''}
|
|
|
78
78
|
fs.writeFileSync(actualFile, content);
|
|
79
79
|
};
|
|
80
80
|
/**
|
|
81
|
-
* Write a revert SQL file for a
|
|
81
|
+
* Write a revert SQL file for a PGPM change
|
|
82
82
|
*/
|
|
83
83
|
const writeRevert = (row, opts) => {
|
|
84
84
|
const globalOpts = getEnvOptions({
|
|
@@ -105,3 +105,7 @@ ${useTx ? 'COMMIT;' : ''}
|
|
|
105
105
|
`;
|
|
106
106
|
fs.writeFileSync(actualFile, content);
|
|
107
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* @deprecated Use writePgpmFiles instead. This alias is kept for backwards compatibility.
|
|
110
|
+
*/
|
|
111
|
+
export const writeSqitchFiles = writePgpmFiles;
|
package/esm/modules/modules.js
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import { getLatestChange } from '../files';
|
|
2
2
|
import { errors } from '@pgpmjs/types';
|
|
3
|
+
/**
|
|
4
|
+
* Mapping from control file names (used in extensions list) to npm package names.
|
|
5
|
+
* Only includes modules that can be installed via pgpm install from @pgpm/* packages.
|
|
6
|
+
* Native PostgreSQL extensions (plpgsql, uuid-ossp, etc.) are not included.
|
|
7
|
+
*/
|
|
8
|
+
export const PGPM_MODULE_MAP = {
|
|
9
|
+
'pgpm-base32': '@pgpm/base32',
|
|
10
|
+
'pgpm-database-jobs': '@pgpm/database-jobs',
|
|
11
|
+
'db-meta-modules': '@pgpm/db-meta-modules',
|
|
12
|
+
'db-meta-schema': '@pgpm/db-meta-schema',
|
|
13
|
+
'pgpm-inflection': '@pgpm/inflection',
|
|
14
|
+
'pgpm-jwt-claims': '@pgpm/jwt-claims',
|
|
15
|
+
'pgpm-stamps': '@pgpm/stamps',
|
|
16
|
+
'pgpm-totp': '@pgpm/totp',
|
|
17
|
+
'pgpm-types': '@pgpm/types',
|
|
18
|
+
'pgpm-utils': '@pgpm/utils',
|
|
19
|
+
'pgpm-uuid': '@pgpm/uuid'
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Determines which pgpm modules from an extensions list are missing from the installed modules.
|
|
23
|
+
* Only checks modules that are in PGPM_MODULE_MAP (installable via pgpm install).
|
|
24
|
+
*
|
|
25
|
+
* @param extensions - List of extension/control file names to check
|
|
26
|
+
* @param installedModules - List of installed npm package names (e.g., '@pgpm/base32')
|
|
27
|
+
* @returns Array of missing modules with their control names and npm package names
|
|
28
|
+
*/
|
|
29
|
+
export const getMissingInstallableModules = (extensions, installedModules) => {
|
|
30
|
+
const missingModules = [];
|
|
31
|
+
for (const ext of extensions) {
|
|
32
|
+
const npmName = PGPM_MODULE_MAP[ext];
|
|
33
|
+
if (npmName && !installedModules.includes(npmName)) {
|
|
34
|
+
missingModules.push({
|
|
35
|
+
controlName: ext,
|
|
36
|
+
npmName
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return missingModules;
|
|
41
|
+
};
|
|
3
42
|
/**
|
|
4
43
|
* Get the latest change from the pgpm.plan file for a specific module.
|
|
5
44
|
*/
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { PgpmOptions } from '@pgpmjs/types';
|
|
2
2
|
import { PgpmPackage } from '../core/class/pgpm';
|
|
3
|
+
/**
|
|
4
|
+
* Prompter interface for interactive prompts.
|
|
5
|
+
* Compatible with Inquirerer from the CLI.
|
|
6
|
+
*/
|
|
7
|
+
interface Prompter {
|
|
8
|
+
prompt: (argv: any, questions: any[]) => Promise<Record<string, any>>;
|
|
9
|
+
}
|
|
3
10
|
interface ExportOptions {
|
|
4
11
|
project: PgpmPackage;
|
|
5
12
|
options: PgpmOptions;
|
|
6
13
|
dbInfo: {
|
|
7
14
|
dbname: string;
|
|
15
|
+
databaseName: string;
|
|
8
16
|
database_ids: string[];
|
|
9
17
|
};
|
|
10
18
|
author: string;
|
|
11
19
|
outdir: string;
|
|
12
20
|
schema_names: string[];
|
|
13
21
|
extensionName?: string;
|
|
22
|
+
extensionDesc?: string;
|
|
14
23
|
metaExtensionName: string;
|
|
24
|
+
metaExtensionDesc?: string;
|
|
25
|
+
prompter?: Prompter;
|
|
15
26
|
}
|
|
16
|
-
export declare const exportMigrations: ({ project, options, dbInfo, author, outdir, schema_names, extensionName, metaExtensionName }: ExportOptions) => Promise<void>;
|
|
27
|
+
export declare const exportMigrations: ({ project, options, dbInfo, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }: ExportOptions) => Promise<void>;
|
|
17
28
|
export {};
|
|
@@ -9,9 +9,95 @@ const glob_1 = require("glob");
|
|
|
9
9
|
const komoji_1 = require("komoji");
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const pg_cache_1 = require("pg-cache");
|
|
12
|
+
const pgpm_1 = require("../core/class/pgpm");
|
|
12
13
|
const files_1 = require("../files");
|
|
14
|
+
const modules_1 = require("../modules/modules");
|
|
13
15
|
const export_meta_1 = require("./export-meta");
|
|
14
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Required extensions for database schema exports.
|
|
18
|
+
* Includes native PostgreSQL extensions and pgpm modules.
|
|
19
|
+
*/
|
|
20
|
+
const DB_REQUIRED_EXTENSIONS = [
|
|
21
|
+
'plpgsql',
|
|
22
|
+
'uuid-ossp',
|
|
23
|
+
'citext',
|
|
24
|
+
'pgcrypto',
|
|
25
|
+
'btree_gist',
|
|
26
|
+
'postgis',
|
|
27
|
+
'hstore',
|
|
28
|
+
'db-meta-schema',
|
|
29
|
+
'pgpm-inflection',
|
|
30
|
+
'pgpm-uuid',
|
|
31
|
+
'pgpm-utils',
|
|
32
|
+
'pgpm-database-jobs',
|
|
33
|
+
'pgpm-jwt-claims',
|
|
34
|
+
'pgpm-stamps',
|
|
35
|
+
'pgpm-base32',
|
|
36
|
+
'pgpm-totp',
|
|
37
|
+
'pgpm-types'
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Required extensions for service/meta exports.
|
|
41
|
+
* Includes native PostgreSQL extensions and pgpm modules for metadata management.
|
|
42
|
+
*/
|
|
43
|
+
const SERVICE_REQUIRED_EXTENSIONS = [
|
|
44
|
+
'plpgsql',
|
|
45
|
+
'db-meta-schema',
|
|
46
|
+
'db-meta-modules'
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Checks which pgpm modules from the extensions list are missing from the workspace
|
|
50
|
+
* and prompts the user if they want to install them.
|
|
51
|
+
*
|
|
52
|
+
* This function only does detection and prompting - it does NOT install.
|
|
53
|
+
* Use installMissingModules() after the module is created to do the actual installation.
|
|
54
|
+
*
|
|
55
|
+
* @param project - The PgpmPackage instance (only needs workspace context)
|
|
56
|
+
* @param extensions - List of extension names (control file names)
|
|
57
|
+
* @param prompter - Optional prompter for interactive confirmation
|
|
58
|
+
* @returns Object with missing modules and whether user wants to install them
|
|
59
|
+
*/
|
|
60
|
+
const detectMissingModules = async (project, extensions, prompter) => {
|
|
61
|
+
// Use workspace-level check - doesn't require being inside a module
|
|
62
|
+
const installed = project.getWorkspaceInstalledModules();
|
|
63
|
+
const missingModules = (0, modules_1.getMissingInstallableModules)(extensions, installed);
|
|
64
|
+
if (missingModules.length === 0) {
|
|
65
|
+
return { missingModules: [], shouldInstall: false };
|
|
66
|
+
}
|
|
67
|
+
const missingNames = missingModules.map(m => m.npmName);
|
|
68
|
+
console.log(`\nMissing pgpm modules detected: ${missingNames.join(', ')}`);
|
|
69
|
+
if (prompter) {
|
|
70
|
+
const { install } = await prompter.prompt({}, [
|
|
71
|
+
{
|
|
72
|
+
type: 'confirm',
|
|
73
|
+
name: 'install',
|
|
74
|
+
message: `Install missing modules (${missingNames.join(', ')})?`,
|
|
75
|
+
default: true
|
|
76
|
+
}
|
|
77
|
+
]);
|
|
78
|
+
return { missingModules, shouldInstall: install };
|
|
79
|
+
}
|
|
80
|
+
return { missingModules, shouldInstall: false };
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Installs missing modules into a specific module directory.
|
|
84
|
+
* Must be called after the module has been created.
|
|
85
|
+
*
|
|
86
|
+
* @param moduleDir - The directory of the module to install into
|
|
87
|
+
* @param missingModules - Array of missing modules to install
|
|
88
|
+
*/
|
|
89
|
+
const installMissingModules = async (moduleDir, missingModules) => {
|
|
90
|
+
if (missingModules.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const missingNames = missingModules.map(m => m.npmName);
|
|
94
|
+
console.log('Installing missing modules...');
|
|
95
|
+
// Create a new PgpmPackage instance pointing to the module directory
|
|
96
|
+
const moduleProject = new pgpm_1.PgpmPackage(moduleDir);
|
|
97
|
+
await moduleProject.installModules(...missingNames);
|
|
98
|
+
console.log('Modules installed successfully.');
|
|
99
|
+
};
|
|
100
|
+
const exportMigrationsToDisk = async ({ project, options, database, databaseId, databaseName, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }) => {
|
|
15
101
|
outdir = outdir + '/';
|
|
16
102
|
const pgPool = (0, pg_cache_1.getPgPool)({
|
|
17
103
|
...options.pg,
|
|
@@ -39,47 +125,51 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
|
|
|
39
125
|
outdir,
|
|
40
126
|
author
|
|
41
127
|
};
|
|
128
|
+
// Build description for the database extension package
|
|
129
|
+
const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
|
|
42
130
|
if (results?.rows?.length > 0) {
|
|
43
|
-
|
|
131
|
+
// Detect missing modules at workspace level and prompt user
|
|
132
|
+
const dbMissingResult = await detectMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter);
|
|
133
|
+
// Create/prepare the module directory
|
|
134
|
+
const dbModuleDir = await preparePackage({
|
|
44
135
|
project,
|
|
45
136
|
author,
|
|
46
137
|
outdir,
|
|
47
138
|
name,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'citext',
|
|
52
|
-
'pgcrypto',
|
|
53
|
-
'btree_gist',
|
|
54
|
-
'postgis',
|
|
55
|
-
'hstore',
|
|
56
|
-
'db-meta-schema',
|
|
57
|
-
'pgpm-inflection',
|
|
58
|
-
'pgpm-uuid',
|
|
59
|
-
'pgpm-utils',
|
|
60
|
-
'pgpm-database-jobs',
|
|
61
|
-
'pgpm-jwt-claims',
|
|
62
|
-
'pgpm-stamps',
|
|
63
|
-
'pgpm-base32',
|
|
64
|
-
'pgpm-totp',
|
|
65
|
-
'pgpm-types'
|
|
66
|
-
]
|
|
139
|
+
description: dbExtensionDesc,
|
|
140
|
+
extensions: [...DB_REQUIRED_EXTENSIONS],
|
|
141
|
+
prompter
|
|
67
142
|
});
|
|
68
|
-
|
|
69
|
-
(
|
|
143
|
+
// Install missing modules if user confirmed (now that module exists)
|
|
144
|
+
if (dbMissingResult.shouldInstall) {
|
|
145
|
+
await installMissingModules(dbModuleDir, dbMissingResult.missingModules);
|
|
146
|
+
}
|
|
147
|
+
(0, files_1.writePgpmPlan)(results.rows, opts);
|
|
148
|
+
(0, files_1.writePgpmFiles)(results.rows, opts);
|
|
70
149
|
let meta = await (0, export_meta_1.exportMeta)({
|
|
71
150
|
opts: options,
|
|
72
151
|
dbname: database,
|
|
73
152
|
database_id: databaseId
|
|
74
153
|
});
|
|
75
154
|
meta = replacer(meta);
|
|
76
|
-
|
|
155
|
+
// Build description for the meta/service extension package
|
|
156
|
+
const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
|
|
157
|
+
// Detect missing modules at workspace level and prompt user
|
|
158
|
+
const svcMissingResult = await detectMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter);
|
|
159
|
+
// Create/prepare the module directory
|
|
160
|
+
const svcModuleDir = await preparePackage({
|
|
77
161
|
project,
|
|
78
162
|
author,
|
|
79
163
|
outdir,
|
|
80
|
-
|
|
81
|
-
|
|
164
|
+
name: metaExtensionName,
|
|
165
|
+
description: metaDesc,
|
|
166
|
+
extensions: [...SERVICE_REQUIRED_EXTENSIONS],
|
|
167
|
+
prompter
|
|
82
168
|
});
|
|
169
|
+
// Install missing modules if user confirmed (now that module exists)
|
|
170
|
+
if (svcMissingResult.shouldInstall) {
|
|
171
|
+
await installMissingModules(svcModuleDir, svcMissingResult.missingModules);
|
|
172
|
+
}
|
|
83
173
|
const metaReplacer = makeReplacer({
|
|
84
174
|
schemas: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name)),
|
|
85
175
|
name: metaExtensionName
|
|
@@ -106,11 +196,15 @@ $LQLMIGRATION$;
|
|
|
106
196
|
|
|
107
197
|
${meta}
|
|
108
198
|
|
|
109
|
-
UPDATE
|
|
110
|
-
|
|
199
|
+
-- TODO: Research needed - These UPDATE statements may be a security leak.
|
|
200
|
+
-- They appear to rebind exported metadata to the target database after import,
|
|
201
|
+
-- but exposing dbname in meta_public tables could leak internal database names.
|
|
202
|
+
-- Consider removing entirely or gating behind an explicit flag.
|
|
203
|
+
-- UPDATE meta_public.apis
|
|
204
|
+
-- SET dbname = current_database() WHERE TRUE;
|
|
111
205
|
|
|
112
|
-
UPDATE meta_public.sites
|
|
113
|
-
|
|
206
|
+
-- UPDATE meta_public.sites
|
|
207
|
+
-- SET dbname = current_database() WHERE TRUE;
|
|
114
208
|
|
|
115
209
|
SET session_replication_role TO DEFAULT;
|
|
116
210
|
`
|
|
@@ -118,51 +212,79 @@ SET session_replication_role TO DEFAULT;
|
|
|
118
212
|
];
|
|
119
213
|
opts.replacer = metaReplacer.replacer;
|
|
120
214
|
opts.name = metaExtensionName;
|
|
121
|
-
(0, files_1.
|
|
122
|
-
(0, files_1.
|
|
215
|
+
(0, files_1.writePgpmPlan)(metaPackage, opts);
|
|
216
|
+
(0, files_1.writePgpmFiles)(metaPackage, opts);
|
|
123
217
|
}
|
|
124
218
|
pgPool.end();
|
|
125
219
|
};
|
|
126
|
-
const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, metaExtensionName }) => {
|
|
220
|
+
const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }) => {
|
|
127
221
|
for (let v = 0; v < dbInfo.database_ids.length; v++) {
|
|
128
222
|
const databaseId = dbInfo.database_ids[v];
|
|
129
223
|
await exportMigrationsToDisk({
|
|
130
224
|
project,
|
|
131
225
|
options,
|
|
132
226
|
extensionName,
|
|
227
|
+
extensionDesc,
|
|
133
228
|
metaExtensionName,
|
|
229
|
+
metaExtensionDesc,
|
|
134
230
|
database: dbInfo.dbname,
|
|
231
|
+
databaseName: dbInfo.databaseName,
|
|
135
232
|
databaseId,
|
|
136
233
|
schema_names,
|
|
137
234
|
author,
|
|
138
|
-
outdir
|
|
235
|
+
outdir,
|
|
236
|
+
prompter
|
|
139
237
|
});
|
|
140
238
|
}
|
|
141
239
|
};
|
|
142
240
|
exports.exportMigrations = exportMigrations;
|
|
143
241
|
/**
|
|
144
|
-
* Creates a
|
|
242
|
+
* Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
|
|
243
|
+
* If the module already exists and a prompter is provided, prompts the user for confirmation.
|
|
244
|
+
*
|
|
245
|
+
* @returns The absolute path to the created/prepared module directory
|
|
145
246
|
*/
|
|
146
|
-
const preparePackage = async ({ project, author, outdir, name, extensions }) => {
|
|
247
|
+
const preparePackage = async ({ project, author, outdir, name, description, extensions, prompter }) => {
|
|
147
248
|
const curDir = process.cwd();
|
|
148
|
-
const
|
|
149
|
-
(0, fs_1.mkdirSync)(
|
|
150
|
-
process.chdir(
|
|
151
|
-
const plan = (0, glob_1.sync)(path_1.default.join(
|
|
249
|
+
const pgpmDir = path_1.default.resolve(path_1.default.join(outdir, name));
|
|
250
|
+
(0, fs_1.mkdirSync)(pgpmDir, { recursive: true });
|
|
251
|
+
process.chdir(pgpmDir);
|
|
252
|
+
const plan = (0, glob_1.sync)(path_1.default.join(pgpmDir, 'pgpm.plan'));
|
|
152
253
|
if (!plan.length) {
|
|
153
254
|
await project.initModule({
|
|
154
255
|
name,
|
|
155
|
-
description
|
|
256
|
+
description,
|
|
156
257
|
author,
|
|
157
258
|
extensions,
|
|
259
|
+
answers: {
|
|
260
|
+
moduleName: name,
|
|
261
|
+
moduleDesc: description,
|
|
262
|
+
access: 'restricted',
|
|
263
|
+
license: 'CLOSED'
|
|
264
|
+
}
|
|
158
265
|
});
|
|
159
266
|
}
|
|
160
267
|
else {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
268
|
+
if (prompter) {
|
|
269
|
+
const { overwrite } = await prompter.prompt({}, [
|
|
270
|
+
{
|
|
271
|
+
type: 'confirm',
|
|
272
|
+
name: 'overwrite',
|
|
273
|
+
message: `Module "${name}" already exists at ${pgpmDir}. Overwrite deploy/revert/verify directories?`,
|
|
274
|
+
default: false
|
|
275
|
+
}
|
|
276
|
+
]);
|
|
277
|
+
if (!overwrite) {
|
|
278
|
+
process.chdir(curDir);
|
|
279
|
+
throw new Error(`Export cancelled: Module "${name}" already exists.`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
(0, fs_1.rmSync)(path_1.default.resolve(pgpmDir, 'deploy'), { recursive: true, force: true });
|
|
283
|
+
(0, fs_1.rmSync)(path_1.default.resolve(pgpmDir, 'revert'), { recursive: true, force: true });
|
|
284
|
+
(0, fs_1.rmSync)(path_1.default.resolve(pgpmDir, 'verify'), { recursive: true, force: true });
|
|
164
285
|
}
|
|
165
286
|
process.chdir(curDir);
|
|
287
|
+
return pgpmDir;
|
|
166
288
|
};
|
|
167
289
|
/**
|
|
168
290
|
* Generates a function for replacing schema names and extension names in strings.
|
package/files/plan/writer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Change,
|
|
1
|
+
import { Change, PgpmRow, Tag, ExtendedPlanFile } from '../types';
|
|
2
2
|
export interface PlanWriteOptions {
|
|
3
3
|
outdir: string;
|
|
4
4
|
name: string;
|
|
@@ -6,9 +6,9 @@ export interface PlanWriteOptions {
|
|
|
6
6
|
author?: string;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Write a
|
|
9
|
+
* Write a PGPM plan file based on the provided rows
|
|
10
10
|
*/
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function writePgpmPlan(rows: PgpmRow[], opts: PlanWriteOptions): void;
|
|
12
12
|
/**
|
|
13
13
|
* Write a plan file with the provided content
|
|
14
14
|
*/
|
|
@@ -25,3 +25,7 @@ export declare function generateChangeLineContent(change: Change): string;
|
|
|
25
25
|
* Generate a line for a tag in a plan file
|
|
26
26
|
*/
|
|
27
27
|
export declare function generateTagLineContent(tag: Tag): string;
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated Use writePgpmPlan instead. This alias is kept for backwards compatibility.
|
|
30
|
+
*/
|
|
31
|
+
export declare const writeSqitchPlan: typeof writePgpmPlan;
|
package/files/plan/writer.js
CHANGED
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.writeSqitchPlan =
|
|
6
|
+
exports.writeSqitchPlan = void 0;
|
|
7
|
+
exports.writePgpmPlan = writePgpmPlan;
|
|
7
8
|
exports.writePlanFile = writePlanFile;
|
|
8
9
|
exports.generatePlanFileContent = generatePlanFileContent;
|
|
9
10
|
exports.generateChangeLineContent = generateChangeLineContent;
|
|
@@ -11,9 +12,9 @@ exports.generateTagLineContent = generateTagLineContent;
|
|
|
11
12
|
const fs_1 = __importDefault(require("fs"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
13
14
|
/**
|
|
14
|
-
* Write a
|
|
15
|
+
* Write a PGPM plan file based on the provided rows
|
|
15
16
|
*/
|
|
16
|
-
function
|
|
17
|
+
function writePgpmPlan(rows, opts) {
|
|
17
18
|
const dir = path_1.default.resolve(path_1.default.join(opts.outdir, opts.name));
|
|
18
19
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
19
20
|
const date = () => '2017-08-11T08:11:51Z'; // stubbed timestamp
|
|
@@ -136,3 +137,7 @@ function generateTagLineContent(tag) {
|
|
|
136
137
|
}
|
|
137
138
|
return line;
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* @deprecated Use writePgpmPlan instead. This alias is kept for backwards compatibility.
|
|
142
|
+
*/
|
|
143
|
+
exports.writeSqitchPlan = writePgpmPlan;
|
package/files/sql/writer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PgpmRow } from '../types';
|
|
2
2
|
export interface SqlWriteOptions {
|
|
3
3
|
outdir: string;
|
|
4
4
|
name: string;
|
|
@@ -7,6 +7,10 @@ export interface SqlWriteOptions {
|
|
|
7
7
|
useTx?: boolean;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Write SQL files for
|
|
10
|
+
* Write SQL files for PGPM migrations (deploy, revert, verify)
|
|
11
11
|
*/
|
|
12
|
-
export declare const
|
|
12
|
+
export declare const writePgpmFiles: (rows: PgpmRow[], opts: SqlWriteOptions) => void;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Use writePgpmFiles instead. This alias is kept for backwards compatibility.
|
|
15
|
+
*/
|
|
16
|
+
export declare const writeSqitchFiles: (rows: PgpmRow[], opts: SqlWriteOptions) => void;
|
package/files/sql/writer.js
CHANGED
|
@@ -3,19 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.writeSqitchFiles = void 0;
|
|
6
|
+
exports.writeSqitchFiles = exports.writePgpmFiles = void 0;
|
|
7
7
|
const env_1 = require("@pgpmjs/env");
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
/**
|
|
11
|
-
* Write SQL files for
|
|
11
|
+
* Write SQL files for PGPM migrations (deploy, revert, verify)
|
|
12
12
|
*/
|
|
13
|
-
const
|
|
13
|
+
const writePgpmFiles = (rows, opts) => {
|
|
14
14
|
rows.forEach((row) => writeVerify(row, opts));
|
|
15
15
|
rows.forEach((row) => writeRevert(row, opts));
|
|
16
16
|
rows.forEach((row) => writeDeploy(row, opts));
|
|
17
17
|
};
|
|
18
|
-
exports.
|
|
18
|
+
exports.writePgpmFiles = writePgpmFiles;
|
|
19
19
|
/**
|
|
20
20
|
* Sort dependencies in a consistent order
|
|
21
21
|
*/
|
|
@@ -25,7 +25,7 @@ const ordered = (arr) => {
|
|
|
25
25
|
return arr.sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
28
|
-
* Write a deploy SQL file for a
|
|
28
|
+
* Write a deploy SQL file for a PGPM change
|
|
29
29
|
*/
|
|
30
30
|
const writeDeploy = (row, opts) => {
|
|
31
31
|
const globalOpts = (0, env_1.getEnvOptions)({
|
|
@@ -57,7 +57,7 @@ ${useTx ? 'COMMIT;' : ''}
|
|
|
57
57
|
fs_1.default.writeFileSync(actualFile, content);
|
|
58
58
|
};
|
|
59
59
|
/**
|
|
60
|
-
* Write a verify SQL file for a
|
|
60
|
+
* Write a verify SQL file for a PGPM change
|
|
61
61
|
*/
|
|
62
62
|
const writeVerify = (row, opts) => {
|
|
63
63
|
const globalOpts = (0, env_1.getEnvOptions)({
|
|
@@ -85,7 +85,7 @@ ${useTx ? 'COMMIT;' : ''}
|
|
|
85
85
|
fs_1.default.writeFileSync(actualFile, content);
|
|
86
86
|
};
|
|
87
87
|
/**
|
|
88
|
-
* Write a revert SQL file for a
|
|
88
|
+
* Write a revert SQL file for a PGPM change
|
|
89
89
|
*/
|
|
90
90
|
const writeRevert = (row, opts) => {
|
|
91
91
|
const globalOpts = (0, env_1.getEnvOptions)({
|
|
@@ -112,3 +112,7 @@ ${useTx ? 'COMMIT;' : ''}
|
|
|
112
112
|
`;
|
|
113
113
|
fs_1.default.writeFileSync(actualFile, content);
|
|
114
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* @deprecated Use writePgpmFiles instead. This alias is kept for backwards compatibility.
|
|
117
|
+
*/
|
|
118
|
+
exports.writeSqitchFiles = exports.writePgpmFiles;
|
package/files/types/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface ResolvedReference {
|
|
|
36
36
|
target: string;
|
|
37
37
|
resolved?: string;
|
|
38
38
|
}
|
|
39
|
-
export interface
|
|
39
|
+
export interface PgpmRow {
|
|
40
40
|
deploy: string;
|
|
41
41
|
revert?: string;
|
|
42
42
|
verify?: string;
|
|
@@ -44,3 +44,7 @@ export interface SqitchRow {
|
|
|
44
44
|
deps?: string[];
|
|
45
45
|
name?: string;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated Use PgpmRow instead. This alias is kept for backwards compatibility.
|
|
49
|
+
*/
|
|
50
|
+
export type SqitchRow = PgpmRow;
|
package/modules/modules.d.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { Module } from '../files';
|
|
2
2
|
export type ModuleMap = Record<string, Module>;
|
|
3
|
+
/**
|
|
4
|
+
* Mapping from control file names (used in extensions list) to npm package names.
|
|
5
|
+
* Only includes modules that can be installed via pgpm install from @pgpm/* packages.
|
|
6
|
+
* Native PostgreSQL extensions (plpgsql, uuid-ossp, etc.) are not included.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PGPM_MODULE_MAP: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Result of checking for missing installable modules.
|
|
11
|
+
*/
|
|
12
|
+
export interface MissingModule {
|
|
13
|
+
controlName: string;
|
|
14
|
+
npmName: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Determines which pgpm modules from an extensions list are missing from the installed modules.
|
|
18
|
+
* Only checks modules that are in PGPM_MODULE_MAP (installable via pgpm install).
|
|
19
|
+
*
|
|
20
|
+
* @param extensions - List of extension/control file names to check
|
|
21
|
+
* @param installedModules - List of installed npm package names (e.g., '@pgpm/base32')
|
|
22
|
+
* @returns Array of missing modules with their control names and npm package names
|
|
23
|
+
*/
|
|
24
|
+
export declare const getMissingInstallableModules: (extensions: string[], installedModules: string[]) => MissingModule[];
|
|
3
25
|
/**
|
|
4
26
|
* Get the latest change from the pgpm.plan file for a specific module.
|
|
5
27
|
*/
|
package/modules/modules.js
CHANGED
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getExtensionsAndModulesChanges = exports.getExtensionsAndModules = exports.latestChangeAndVersion = exports.latestChange = void 0;
|
|
3
|
+
exports.getExtensionsAndModulesChanges = exports.getExtensionsAndModules = exports.latestChangeAndVersion = exports.latestChange = exports.getMissingInstallableModules = exports.PGPM_MODULE_MAP = void 0;
|
|
4
4
|
const files_1 = require("../files");
|
|
5
5
|
const types_1 = require("@pgpmjs/types");
|
|
6
|
+
/**
|
|
7
|
+
* Mapping from control file names (used in extensions list) to npm package names.
|
|
8
|
+
* Only includes modules that can be installed via pgpm install from @pgpm/* packages.
|
|
9
|
+
* Native PostgreSQL extensions (plpgsql, uuid-ossp, etc.) are not included.
|
|
10
|
+
*/
|
|
11
|
+
exports.PGPM_MODULE_MAP = {
|
|
12
|
+
'pgpm-base32': '@pgpm/base32',
|
|
13
|
+
'pgpm-database-jobs': '@pgpm/database-jobs',
|
|
14
|
+
'db-meta-modules': '@pgpm/db-meta-modules',
|
|
15
|
+
'db-meta-schema': '@pgpm/db-meta-schema',
|
|
16
|
+
'pgpm-inflection': '@pgpm/inflection',
|
|
17
|
+
'pgpm-jwt-claims': '@pgpm/jwt-claims',
|
|
18
|
+
'pgpm-stamps': '@pgpm/stamps',
|
|
19
|
+
'pgpm-totp': '@pgpm/totp',
|
|
20
|
+
'pgpm-types': '@pgpm/types',
|
|
21
|
+
'pgpm-utils': '@pgpm/utils',
|
|
22
|
+
'pgpm-uuid': '@pgpm/uuid'
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Determines which pgpm modules from an extensions list are missing from the installed modules.
|
|
26
|
+
* Only checks modules that are in PGPM_MODULE_MAP (installable via pgpm install).
|
|
27
|
+
*
|
|
28
|
+
* @param extensions - List of extension/control file names to check
|
|
29
|
+
* @param installedModules - List of installed npm package names (e.g., '@pgpm/base32')
|
|
30
|
+
* @returns Array of missing modules with their control names and npm package names
|
|
31
|
+
*/
|
|
32
|
+
const getMissingInstallableModules = (extensions, installedModules) => {
|
|
33
|
+
const missingModules = [];
|
|
34
|
+
for (const ext of extensions) {
|
|
35
|
+
const npmName = exports.PGPM_MODULE_MAP[ext];
|
|
36
|
+
if (npmName && !installedModules.includes(npmName)) {
|
|
37
|
+
missingModules.push({
|
|
38
|
+
controlName: ext,
|
|
39
|
+
npmName
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return missingModules;
|
|
44
|
+
};
|
|
45
|
+
exports.getMissingInstallableModules = getMissingInstallableModules;
|
|
6
46
|
/**
|
|
7
47
|
* Get the latest change from the pgpm.plan file for a specific module.
|
|
8
48
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/core",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM Package and Migration Tools",
|
|
6
6
|
"main": "index.js",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"pgsql-parser": "^17.9.5",
|
|
65
65
|
"yanse": "^0.1.8"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "5b93211b63e72bedcfabc7c4aa41c69ed33d0f6c"
|
|
68
68
|
}
|