Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions pgpm/cli/src/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ export default async (
]);

const outdir = resolve(project.workspacePath, 'packages/');

prompter.close();

await exportMigrations({
project,
Expand All @@ -139,9 +137,12 @@ export default async (
schema_names,
outdir,
extensionName,
metaExtensionName
metaExtensionName,
prompter
});

prompter.close();

console.log(`

|||
Expand Down
146 changes: 122 additions & 24 deletions pgpm/core/src/export/export-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,96 @@ import { getPgPool } from 'pg-cache';

import { PgpmPackage } from '../core/class/pgpm';
import { PgpmRow, SqlWriteOptions, writePgpmFiles, writePgpmPlan } from '../files';
import { getMissingInstallableModules } from '../modules/modules';
import { exportMeta } from './export-meta';

/**
* Required extensions for database schema exports.
* Includes native PostgreSQL extensions and pgpm modules.
*/
const DB_REQUIRED_EXTENSIONS = [
'plpgsql',
'uuid-ossp',
'citext',
'pgcrypto',
'btree_gist',
'postgis',
'hstore',
'db-meta-schema',
'pgpm-inflection',
'pgpm-uuid',
'pgpm-utils',
'pgpm-database-jobs',
'pgpm-jwt-claims',
'pgpm-stamps',
'pgpm-base32',
'pgpm-totp',
'pgpm-types'
] as const;

/**
* Required extensions for service/meta exports.
* Includes native PostgreSQL extensions and pgpm modules for metadata management.
*/
const SERVICE_REQUIRED_EXTENSIONS = [
'plpgsql',
'db-meta-schema',
'db-meta-modules'
] as const;

/**
* Prompter interface for interactive prompts.
* Compatible with Inquirerer from the CLI.
*/
interface Prompter {
prompt: (argv: any, questions: any[]) => Promise<Record<string, any>>;
}

/**
* Checks which pgpm modules from the extensions list are missing from the workspace
* and prompts the user to install them if a prompter is provided.
*
* @param project - The PgpmPackage instance
* @param extensions - List of extension names (control file names)
* @param prompter - Optional prompter for interactive confirmation
* @returns List of extensions that were successfully installed
*/
const promptAndInstallMissingModules = async (
project: PgpmPackage,
extensions: string[],
prompter?: Prompter
): Promise<string[]> => {
const { installed } = project.getInstalledModules();
const missingModules = getMissingInstallableModules(extensions, installed);

if (missingModules.length === 0) {
return [];
}

const missingNames = missingModules.map(m => m.npmName);
console.log(`\nMissing pgpm modules detected: ${missingNames.join(', ')}`);

if (prompter) {
const { install } = await prompter.prompt({}, [
{
type: 'confirm',
name: 'install',
message: `Install missing modules (${missingNames.join(', ')})?`,
default: true
}
]);

if (install) {
console.log('Installing missing modules...');
await project.installModules(...missingNames);
console.log('Modules installed successfully.');
return missingModules.map(m => m.controlName);
}
}

return [];
};

interface ExportMigrationsToDiskOptions {
project: PgpmPackage;
options: PgpmOptions;
Expand All @@ -22,6 +110,7 @@ interface ExportMigrationsToDiskOptions {
extensionDesc?: string;
metaExtensionName: string;
metaExtensionDesc?: string;
prompter?: Prompter;
}

interface ExportOptions {
Expand All @@ -39,6 +128,7 @@ interface ExportOptions {
extensionDesc?: string;
metaExtensionName: string;
metaExtensionDesc?: string;
prompter?: Prompter;
}

const exportMigrationsToDisk = async ({
Expand All @@ -53,7 +143,8 @@ const exportMigrationsToDisk = async ({
extensionName,
extensionDesc,
metaExtensionName,
metaExtensionDesc
metaExtensionDesc,
prompter
}: ExportMigrationsToDiskOptions): Promise<void> => {
outdir = outdir + '/';

Expand Down Expand Up @@ -106,31 +197,16 @@ const exportMigrationsToDisk = async ({
const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;

if (results?.rows?.length > 0) {
await promptAndInstallMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter);

await preparePackage({
project,
author,
outdir,
name,
description: dbExtensionDesc,
extensions: [
'plpgsql',
'uuid-ossp',
'citext',
'pgcrypto',
'btree_gist',
'postgis',
'hstore',
'db-meta-schema',
'pgpm-inflection',
'pgpm-uuid',
'pgpm-utils',
'pgpm-database-jobs',
'pgpm-jwt-claims',
'pgpm-stamps',
'pgpm-base32',
'pgpm-totp',
'pgpm-types'
]
extensions: [...DB_REQUIRED_EXTENSIONS],
prompter
});

writePgpmPlan(results.rows, opts);
Expand All @@ -147,13 +223,16 @@ const exportMigrationsToDisk = async ({
// Build description for the meta/service extension package
const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;

await promptAndInstallMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter);

await preparePackage({
project,
author,
outdir,
name: metaExtensionName,
description: metaDesc,
extensions: ['plpgsql', 'db-meta-schema', 'db-meta-modules']
extensions: [...SERVICE_REQUIRED_EXTENSIONS],
prompter
});

const metaReplacer = makeReplacer({
Expand Down Expand Up @@ -220,7 +299,8 @@ export const exportMigrations = async ({
extensionName,
extensionDesc,
metaExtensionName,
metaExtensionDesc
metaExtensionDesc,
prompter
}: ExportOptions): Promise<void> => {
for (let v = 0; v < dbInfo.database_ids.length; v++) {
const databaseId = dbInfo.database_ids[v];
Expand All @@ -236,7 +316,8 @@ export const exportMigrations = async ({
databaseId,
schema_names,
author,
outdir
outdir,
prompter
});
}
};
Expand All @@ -249,6 +330,7 @@ interface PreparePackageOptions {
name: string;
description: string;
extensions: string[];
prompter?: Prompter;
}

interface Schema {
Expand All @@ -268,14 +350,16 @@ interface ReplacerResult {

/**
* Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
* If the module already exists and a prompter is provided, prompts the user for confirmation.
*/
const preparePackage = async ({
project,
author,
outdir,
name,
description,
extensions
extensions,
prompter
}: PreparePackageOptions): Promise<void> => {
const curDir = process.cwd();
const pgpmDir = path.resolve(path.join(outdir, name));
Expand All @@ -297,6 +381,20 @@ const preparePackage = async ({
}
});
} else {
if (prompter) {
const { overwrite } = await prompter.prompt({}, [
{
type: 'confirm',
name: 'overwrite',
message: `Module "${name}" already exists at ${pgpmDir}. Overwrite deploy/revert/verify directories?`,
default: false
}
]);
if (!overwrite) {
process.chdir(curDir);
throw new Error(`Export cancelled: Module "${name}" already exists.`);
}
}
rmSync(path.resolve(pgpmDir, 'deploy'), { recursive: true, force: true });
rmSync(path.resolve(pgpmDir, 'revert'), { recursive: true, force: true });
rmSync(path.resolve(pgpmDir, 'verify'), { recursive: true, force: true });
Expand Down
54 changes: 54 additions & 0 deletions pgpm/core/src/modules/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,60 @@ import { errors } from '@pgpmjs/types';

export type ModuleMap = Record<string, Module>;

/**
* Mapping from control file names (used in extensions list) to npm package names.
* Only includes modules that can be installed via pgpm install from @pgpm/* packages.
* Native PostgreSQL extensions (plpgsql, uuid-ossp, etc.) are not included.
*/
export const PGPM_MODULE_MAP: Record<string, string> = {
'pgpm-base32': '@pgpm/base32',
'pgpm-database-jobs': '@pgpm/database-jobs',
'db-meta-modules': '@pgpm/db-meta-modules',
'db-meta-schema': '@pgpm/db-meta-schema',
'pgpm-inflection': '@pgpm/inflection',
'pgpm-jwt-claims': '@pgpm/jwt-claims',
'pgpm-stamps': '@pgpm/stamps',
'pgpm-totp': '@pgpm/totp',
'pgpm-types': '@pgpm/types',
'pgpm-utils': '@pgpm/utils',
'pgpm-uuid': '@pgpm/uuid'
};

/**
* Result of checking for missing installable modules.
*/
export interface MissingModule {
controlName: string;
npmName: string;
}

/**
* Determines which pgpm modules from an extensions list are missing from the installed modules.
* Only checks modules that are in PGPM_MODULE_MAP (installable via pgpm install).
*
* @param extensions - List of extension/control file names to check
* @param installedModules - List of installed npm package names (e.g., '@pgpm/base32')
* @returns Array of missing modules with their control names and npm package names
*/
export const getMissingInstallableModules = (
extensions: string[],
installedModules: string[]
): MissingModule[] => {
const missingModules: MissingModule[] = [];

for (const ext of extensions) {
const npmName = PGPM_MODULE_MAP[ext];
if (npmName && !installedModules.includes(npmName)) {
missingModules.push({
controlName: ext,
npmName
});
}
}

return missingModules;
};

/**
* Get the latest change from the pgpm.plan file for a specific module.
*/
Expand Down