@pgpmjs/core 4.2.0 → 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,6 +3,7 @@ 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 { PgpmPackage } from '../core/class/pgpm';
6
7
  import { writePgpmFiles, writePgpmPlan } from '../files';
7
8
  import { getMissingInstallableModules } from '../modules/modules';
8
9
  import { exportMeta } from './export-meta';
@@ -40,18 +41,22 @@ const SERVICE_REQUIRED_EXTENSIONS = [
40
41
  ];
41
42
  /**
42
43
  * Checks which pgpm modules from the extensions list are missing from the workspace
43
- * and prompts the user to install them if a prompter is provided.
44
+ * and prompts the user if they want to install them.
44
45
  *
45
- * @param project - The PgpmPackage instance
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)
46
50
  * @param extensions - List of extension names (control file names)
47
51
  * @param prompter - Optional prompter for interactive confirmation
48
- * @returns List of extensions that were successfully installed
52
+ * @returns Object with missing modules and whether user wants to install them
49
53
  */
50
- const promptAndInstallMissingModules = async (project, extensions, prompter) => {
51
- const { installed } = project.getInstalledModules();
54
+ const detectMissingModules = async (project, extensions, prompter) => {
55
+ // Use workspace-level check - doesn't require being inside a module
56
+ const installed = project.getWorkspaceInstalledModules();
52
57
  const missingModules = getMissingInstallableModules(extensions, installed);
53
58
  if (missingModules.length === 0) {
54
- return [];
59
+ return { missingModules: [], shouldInstall: false };
55
60
  }
56
61
  const missingNames = missingModules.map(m => m.npmName);
57
62
  console.log(`\nMissing pgpm modules detected: ${missingNames.join(', ')}`);
@@ -64,14 +69,27 @@ const promptAndInstallMissingModules = async (project, extensions, prompter) =>
64
69
  default: true
65
70
  }
66
71
  ]);
67
- if (install) {
68
- console.log('Installing missing modules...');
69
- await project.installModules(...missingNames);
70
- console.log('Modules installed successfully.');
71
- return missingModules.map(m => m.controlName);
72
- }
72
+ return { missingModules, shouldInstall: install };
73
73
  }
74
- return [];
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.');
75
93
  };
76
94
  const exportMigrationsToDisk = async ({ project, options, database, databaseId, databaseName, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }) => {
77
95
  outdir = outdir + '/';
@@ -104,8 +122,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
104
122
  // Build description for the database extension package
105
123
  const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
106
124
  if (results?.rows?.length > 0) {
107
- await promptAndInstallMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter);
108
- 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({
109
129
  project,
110
130
  author,
111
131
  outdir,
@@ -114,6 +134,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
114
134
  extensions: [...DB_REQUIRED_EXTENSIONS],
115
135
  prompter
116
136
  });
137
+ // Install missing modules if user confirmed (now that module exists)
138
+ if (dbMissingResult.shouldInstall) {
139
+ await installMissingModules(dbModuleDir, dbMissingResult.missingModules);
140
+ }
117
141
  writePgpmPlan(results.rows, opts);
118
142
  writePgpmFiles(results.rows, opts);
119
143
  let meta = await exportMeta({
@@ -124,8 +148,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
124
148
  meta = replacer(meta);
125
149
  // Build description for the meta/service extension package
126
150
  const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
127
- await promptAndInstallMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter);
128
- await preparePackage({
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({
129
155
  project,
130
156
  author,
131
157
  outdir,
@@ -134,6 +160,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
134
160
  extensions: [...SERVICE_REQUIRED_EXTENSIONS],
135
161
  prompter
136
162
  });
163
+ // Install missing modules if user confirmed (now that module exists)
164
+ if (svcMissingResult.shouldInstall) {
165
+ await installMissingModules(svcModuleDir, svcMissingResult.missingModules);
166
+ }
137
167
  const metaReplacer = makeReplacer({
138
168
  schemas: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name)),
139
169
  name: metaExtensionName
@@ -204,6 +234,8 @@ export const exportMigrations = async ({ project, options, dbInfo, author, outdi
204
234
  /**
205
235
  * Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
206
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
207
239
  */
208
240
  const preparePackage = async ({ project, author, outdir, name, description, extensions, prompter }) => {
209
241
  const curDir = process.cwd();
@@ -245,6 +277,7 @@ const preparePackage = async ({ project, author, outdir, name, description, exte
245
277
  rmSync(path.resolve(pgpmDir, 'verify'), { recursive: true, force: true });
246
278
  }
247
279
  process.chdir(curDir);
280
+ return pgpmDir;
248
281
  };
249
282
  /**
250
283
  * Generates a function for replacing schema names and extension names in strings.
@@ -9,6 +9,7 @@ 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");
13
14
  const modules_1 = require("../modules/modules");
14
15
  const export_meta_1 = require("./export-meta");
@@ -46,18 +47,22 @@ const SERVICE_REQUIRED_EXTENSIONS = [
46
47
  ];
47
48
  /**
48
49
  * Checks which pgpm modules from the extensions list are missing from the workspace
49
- * and prompts the user to install them if a prompter is provided.
50
+ * and prompts the user if they want to install them.
50
51
  *
51
- * @param project - The PgpmPackage instance
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)
52
56
  * @param extensions - List of extension names (control file names)
53
57
  * @param prompter - Optional prompter for interactive confirmation
54
- * @returns List of extensions that were successfully installed
58
+ * @returns Object with missing modules and whether user wants to install them
55
59
  */
56
- const promptAndInstallMissingModules = async (project, extensions, prompter) => {
57
- const { installed } = project.getInstalledModules();
60
+ const detectMissingModules = async (project, extensions, prompter) => {
61
+ // Use workspace-level check - doesn't require being inside a module
62
+ const installed = project.getWorkspaceInstalledModules();
58
63
  const missingModules = (0, modules_1.getMissingInstallableModules)(extensions, installed);
59
64
  if (missingModules.length === 0) {
60
- return [];
65
+ return { missingModules: [], shouldInstall: false };
61
66
  }
62
67
  const missingNames = missingModules.map(m => m.npmName);
63
68
  console.log(`\nMissing pgpm modules detected: ${missingNames.join(', ')}`);
@@ -70,14 +75,27 @@ const promptAndInstallMissingModules = async (project, extensions, prompter) =>
70
75
  default: true
71
76
  }
72
77
  ]);
73
- if (install) {
74
- console.log('Installing missing modules...');
75
- await project.installModules(...missingNames);
76
- console.log('Modules installed successfully.');
77
- return missingModules.map(m => m.controlName);
78
- }
78
+ return { missingModules, shouldInstall: install };
79
79
  }
80
- return [];
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.');
81
99
  };
82
100
  const exportMigrationsToDisk = async ({ project, options, database, databaseId, databaseName, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter }) => {
83
101
  outdir = outdir + '/';
@@ -110,8 +128,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
110
128
  // Build description for the database extension package
111
129
  const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
112
130
  if (results?.rows?.length > 0) {
113
- await promptAndInstallMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter);
114
- 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({
115
135
  project,
116
136
  author,
117
137
  outdir,
@@ -120,6 +140,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
120
140
  extensions: [...DB_REQUIRED_EXTENSIONS],
121
141
  prompter
122
142
  });
143
+ // Install missing modules if user confirmed (now that module exists)
144
+ if (dbMissingResult.shouldInstall) {
145
+ await installMissingModules(dbModuleDir, dbMissingResult.missingModules);
146
+ }
123
147
  (0, files_1.writePgpmPlan)(results.rows, opts);
124
148
  (0, files_1.writePgpmFiles)(results.rows, opts);
125
149
  let meta = await (0, export_meta_1.exportMeta)({
@@ -130,8 +154,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
130
154
  meta = replacer(meta);
131
155
  // Build description for the meta/service extension package
132
156
  const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
133
- await promptAndInstallMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter);
134
- await preparePackage({
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({
135
161
  project,
136
162
  author,
137
163
  outdir,
@@ -140,6 +166,10 @@ const exportMigrationsToDisk = async ({ project, options, database, databaseId,
140
166
  extensions: [...SERVICE_REQUIRED_EXTENSIONS],
141
167
  prompter
142
168
  });
169
+ // Install missing modules if user confirmed (now that module exists)
170
+ if (svcMissingResult.shouldInstall) {
171
+ await installMissingModules(svcModuleDir, svcMissingResult.missingModules);
172
+ }
143
173
  const metaReplacer = makeReplacer({
144
174
  schemas: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name)),
145
175
  name: metaExtensionName
@@ -211,6 +241,8 @@ exports.exportMigrations = exportMigrations;
211
241
  /**
212
242
  * Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
213
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
214
246
  */
215
247
  const preparePackage = async ({ project, author, outdir, name, description, extensions, prompter }) => {
216
248
  const curDir = process.cwd();
@@ -252,6 +284,7 @@ const preparePackage = async ({ project, author, outdir, name, description, exte
252
284
  (0, fs_1.rmSync)(path_1.default.resolve(pgpmDir, 'verify'), { recursive: true, force: true });
253
285
  }
254
286
  process.chdir(curDir);
287
+ return pgpmDir;
255
288
  };
256
289
  /**
257
290
  * Generates a function for replacing schema names and extension names in strings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpmjs/core",
3
- "version": "4.2.0",
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": "9b68e2d19937ad4e2a81a5de110a197a8572e3d9"
67
+ "gitHead": "5b93211b63e72bedcfabc7c4aa41c69ed33d0f6c"
68
68
  }