@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.
@@ -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.
@@ -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, 'extensions');
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, 'extensions');
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.
@@ -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, 'extensions');
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, 'extensions');
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 { writeSqitchFiles, writeSqitchPlan } from '../files';
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
- const exportMigrationsToDisk = async ({ project, options, database, databaseId, author, outdir, schema_names, extensionName, metaExtensionName }) => {
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
- await preparePackage({
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
- extensions: [
43
- 'plpgsql',
44
- 'uuid-ossp',
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
- writeSqitchPlan(results.rows, opts);
63
- writeSqitchFiles(results.rows, opts);
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
- await preparePackage({
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
- extensions: ['plpgsql', 'db-meta-schema', 'db-meta-modules'],
75
- name: metaExtensionName
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 meta_public.apis
104
- SET dbname = current_database() WHERE TRUE;
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
- SET dbname = current_database() WHERE TRUE;
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
- writeSqitchPlan(metaPackage, opts);
116
- writeSqitchFiles(metaPackage, opts);
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 Sqitch package directory or resets the deploy/revert/verify directories if it exists.
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 sqitchDir = path.resolve(path.join(outdir, name));
142
- mkdirSync(sqitchDir, { recursive: true });
143
- process.chdir(sqitchDir);
144
- const plan = glob(path.join(sqitchDir, 'pgpm.plan'));
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: name,
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
- rmSync(path.resolve(sqitchDir, 'deploy'), { recursive: true, force: true });
155
- rmSync(path.resolve(sqitchDir, 'revert'), { recursive: true, force: true });
156
- rmSync(path.resolve(sqitchDir, 'verify'), { recursive: true, force: true });
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.
@@ -1,9 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  /**
4
- * Write a Sqitch plan file based on the provided rows
4
+ * Write a PGPM plan file based on the provided rows
5
5
  */
6
- export function writeSqitchPlan(rows, opts) {
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;
@@ -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 Sqitch migrations (deploy, revert, verify)
5
+ * Write SQL files for PGPM migrations (deploy, revert, verify)
6
6
  */
7
- export const writeSqitchFiles = (rows, opts) => {
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 Sqitch change
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 Sqitch change
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 Sqitch change
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;
@@ -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
- const exportMigrationsToDisk = async ({ project, options, database, databaseId, author, outdir, schema_names, extensionName, metaExtensionName }) => {
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
- await preparePackage({
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
- extensions: [
49
- 'plpgsql',
50
- 'uuid-ossp',
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
- (0, files_1.writeSqitchPlan)(results.rows, opts);
69
- (0, files_1.writeSqitchFiles)(results.rows, opts);
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
- await preparePackage({
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
- extensions: ['plpgsql', 'db-meta-schema', 'db-meta-modules'],
81
- name: metaExtensionName
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 meta_public.apis
110
- SET dbname = current_database() WHERE TRUE;
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
- SET dbname = current_database() WHERE TRUE;
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.writeSqitchPlan)(metaPackage, opts);
122
- (0, files_1.writeSqitchFiles)(metaPackage, opts);
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 Sqitch package directory or resets the deploy/revert/verify directories if it exists.
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 sqitchDir = path_1.default.resolve(path_1.default.join(outdir, name));
149
- (0, fs_1.mkdirSync)(sqitchDir, { recursive: true });
150
- process.chdir(sqitchDir);
151
- const plan = (0, glob_1.sync)(path_1.default.join(sqitchDir, 'pgpm.plan'));
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: name,
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
- (0, fs_1.rmSync)(path_1.default.resolve(sqitchDir, 'deploy'), { recursive: true, force: true });
162
- (0, fs_1.rmSync)(path_1.default.resolve(sqitchDir, 'revert'), { recursive: true, force: true });
163
- (0, fs_1.rmSync)(path_1.default.resolve(sqitchDir, 'verify'), { recursive: true, force: true });
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.
@@ -1,4 +1,4 @@
1
- import { Change, SqitchRow, Tag, ExtendedPlanFile } from '../types';
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 Sqitch plan file based on the provided rows
9
+ * Write a PGPM plan file based on the provided rows
10
10
  */
11
- export declare function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void;
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;
@@ -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 = 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 Sqitch plan file based on the provided rows
15
+ * Write a PGPM plan file based on the provided rows
15
16
  */
16
- function writeSqitchPlan(rows, opts) {
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;
@@ -1,4 +1,4 @@
1
- import { SqitchRow } from '../types';
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 Sqitch migrations (deploy, revert, verify)
10
+ * Write SQL files for PGPM migrations (deploy, revert, verify)
11
11
  */
12
- export declare const writeSqitchFiles: (rows: SqitchRow[], opts: SqlWriteOptions) => void;
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;
@@ -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 Sqitch migrations (deploy, revert, verify)
11
+ * Write SQL files for PGPM migrations (deploy, revert, verify)
12
12
  */
13
- const writeSqitchFiles = (rows, opts) => {
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.writeSqitchFiles = writeSqitchFiles;
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 Sqitch change
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 Sqitch change
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 Sqitch change
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;
@@ -36,7 +36,7 @@ export interface ResolvedReference {
36
36
  target: string;
37
37
  resolved?: string;
38
38
  }
39
- export interface SqitchRow {
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;
@@ -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
  */
@@ -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.2",
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": "c2e68f9808a87e3231d9b9af5e706bef5dbd2af8"
67
+ "gitHead": "5b93211b63e72bedcfabc7c4aa41c69ed33d0f6c"
68
68
  }