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
99 changes: 99 additions & 0 deletions packages/cli/__tests__/package-alias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as fs from 'fs';
import * as path from 'path';
import { teardownPgPools } from 'pg-cache';

import { CLIDeployTestFixture } from '../test-utils';

jest.setTimeout(30000);

describe('CLI Package Alias Resolution', () => {
let fixture: CLIDeployTestFixture;
let testDb: any;

beforeAll(async () => {
fixture = new CLIDeployTestFixture('sqitch', 'simple-w-tags');

// Modify the package.json of my-first to have a scoped npm name
// This simulates the case where package.json name differs from control file name
const packageJsonPath = fixture.fixturePath('packages', 'my-first', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
packageJson.name = '@test-scope/my-first';
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
});

beforeEach(async () => {
testDb = await fixture.setupTestDatabase();
});

afterAll(async () => {
await fixture.cleanup();
await teardownPgPools();
});

it('should deploy using npm package name alias instead of control file name', async () => {
// Deploy using the scoped npm name (@test-scope/my-first) instead of control file name (my-first)
const commands = `lql deploy --database ${testDb.name} --package @test-scope/my-first --yes`;

await fixture.runTerminalCommands(commands, {
database: testDb.name
}, true);

// Verify deployment succeeded - the schema should exist
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);

// Verify the deployed changes are recorded under the control file name (my-first), not the npm name
const deployedChanges = await testDb.getDeployedChanges();
expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true);
});

it('should still work with control file name directly (backward compatibility)', async () => {
// Deploy using the control file name directly
const commands = `lql deploy --database ${testDb.name} --package my-first --yes`;

await fixture.runTerminalCommands(commands, {
database: testDb.name
}, true);

// Verify deployment succeeded
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);

const deployedChanges = await testDb.getDeployedChanges();
expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true);
});

it('should deploy to specific change using npm package name alias', async () => {
// Deploy to a specific change using the aliased npm name
const commands = `lql deploy --database ${testDb.name} --package @test-scope/my-first --to schema_myfirstapp --yes`;

await fixture.runTerminalCommands(commands, {
database: testDb.name
}, true);

// Verify only the schema was deployed (not the tables)
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);

const deployedChanges = await testDb.getDeployedChanges();
expect(deployedChanges.find((change: any) =>
change.package === 'my-first' && change.change_name === 'schema_myfirstapp'
)).toBeTruthy();
});

it('should revert using npm package name alias', async () => {
// First deploy
const deployCommands = `lql deploy --database ${testDb.name} --package @test-scope/my-first --yes`;
await fixture.runTerminalCommands(deployCommands, {
database: testDb.name
}, true);

expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);

// Then revert using the aliased npm name
const revertCommands = `lql revert --database ${testDb.name} --package @test-scope/my-first --yes`;
await fixture.runTerminalCommands(revertCommands, {
database: testDb.name
}, true);

// Verify revert succeeded
expect(await testDb.exists('schema', 'myfirstapp')).toBe(false);
});
});
7 changes: 4 additions & 3 deletions packages/pgpm/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
getSpawnEnvWithPg,
} from 'pg-env';

import { getTargetDatabase } from '../utils';
import { getTargetDatabase, resolvePackageAlias } from '../utils';
import { selectPackage } from '../utils/module-utils';

const deployUsageText = `
Expand Down Expand Up @@ -154,9 +154,10 @@ export default async (
} else if (packageName) {
target = packageName;
} else if (argv.package && argv.to) {
target = `${argv.package}:${argv.to}`;
const resolvedPackage = resolvePackageAlias(argv.package as string, cwd);
target = `${resolvedPackage}:${argv.to}`;
} else if (argv.package) {
target = argv.package as string;
target = resolvePackageAlias(argv.package as string, cwd);
}

await project.deploy(
Expand Down
13 changes: 7 additions & 6 deletions packages/pgpm/src/commands/revert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Logger } from '@pgpmjs/logger';
import { CLIOptions, Inquirerer, Question } from 'inquirerer';
import { getPgEnvOptions } from 'pg-env';

import { getTargetDatabase } from '../utils';
import { getTargetDatabase, resolvePackageAlias } from '../utils';
import { cliExitWithError } from '../utils/cli-error';
import { selectDeployedChange, selectDeployedPackage } from '../utils/deployed-changes';

Expand Down Expand Up @@ -84,7 +84,7 @@ export default async (

let packageName: string | undefined;
if (recursive && argv.to !== true) {
packageName = await selectDeployedPackage(database, argv, prompter, log, 'revert');
packageName = await selectDeployedPackage(database, argv, prompter, log, 'revert', cwd);
if (!packageName) {
await cliExitWithError('No package found to revert');
}
Expand All @@ -102,18 +102,19 @@ export default async (
let target: string | undefined;

if (argv.to === true) {
target = await selectDeployedChange(database, argv, prompter, log, 'revert');
target = await selectDeployedChange(database, argv, prompter, log, 'revert', cwd);
if (!target) {
await cliExitWithError('No target selected, operation cancelled');
}
} else if (packageName && argv.to) {
}else if (packageName && argv.to) {
target = `${packageName}:${argv.to}`;
} else if (packageName) {
target = packageName;
} else if (argv.package && argv.to) {
target = `${argv.package}:${argv.to}`;
const resolvedPackage = resolvePackageAlias(argv.package as string, cwd);
target = `${resolvedPackage}:${argv.to}`;
} else if (argv.package) {
target = argv.package as string;
target = resolvePackageAlias(argv.package as string, cwd);
}

await pkg.revert(
Expand Down
3 changes: 2 additions & 1 deletion packages/pgpm/src/commands/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as path from 'path';

import { extractFirst } from '../utils/argv';
import { selectPackage } from '../utils/module-utils';
import { resolvePackageAlias } from '../utils/package-alias';

const log = new Logger('tag');

Expand Down Expand Up @@ -61,7 +62,7 @@ export default async (
let packageName: string | undefined;

if (argv.package) {
packageName = argv.package as string;
packageName = resolvePackageAlias(argv.package as string, cwd);
log.info(`Using specified package: ${packageName}`);
}
else if (pkg.isInModule()) {
Expand Down
13 changes: 7 additions & 6 deletions packages/pgpm/src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Logger } from '@pgpmjs/logger';
import { CLIOptions, Inquirerer, Question } from 'inquirerer';
import { getPgEnvOptions } from 'pg-env';

import { getTargetDatabase } from '../utils';
import { getTargetDatabase, resolvePackageAlias } from '../utils';
import { cliExitWithError } from '../utils/cli-error';
import { selectDeployedChange, selectDeployedPackage } from '../utils/deployed-changes';

Expand Down Expand Up @@ -62,7 +62,7 @@ export default async (

let packageName: string | undefined;
if (recursive && argv.to !== true) {
packageName = await selectDeployedPackage(database, argv, prompter, log, 'verify');
packageName = await selectDeployedPackage(database, argv, prompter, log, 'verify', cwd);
if (!packageName) {
await cliExitWithError('No package found to verify');
}
Expand All @@ -77,18 +77,19 @@ export default async (
let target: string | undefined;

if (argv.to === true) {
target = await selectDeployedChange(database, argv, prompter, log, 'verify');
target = await selectDeployedChange(database, argv, prompter, log, 'verify', cwd);
if (!target) {
await cliExitWithError('No target selected, operation cancelled');
}
} else if (packageName && argv.to) {
}else if (packageName && argv.to) {
target = `${packageName}:${argv.to}`;
} else if (packageName) {
target = packageName;
} else if (argv.package && argv.to) {
target = `${argv.package}:${argv.to}`;
const resolvedPackage = resolvePackageAlias(argv.package as string, cwd);
target = `${resolvedPackage}:${argv.to}`;
} else if (argv.package) {
target = argv.package as string;
target = resolvePackageAlias(argv.package as string, cwd);
}

await project.verify(
Expand Down
12 changes: 8 additions & 4 deletions packages/pgpm/src/utils/deployed-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import { Logger } from '@pgpmjs/logger';
import { Inquirerer } from 'inquirerer';
import { getPgEnvOptions } from 'pg-env';

import { resolvePackageAlias } from './package-alias';

export async function selectDeployedChange(
database: string,
argv: Partial<Record<string, any>>,
prompter: Inquirerer,
log: Logger,
action: 'revert' | 'verify' = 'revert'
action: 'revert' | 'verify' = 'revert',
cwd: string = process.cwd()
): Promise<string | undefined> {
const pgEnv = getPgEnvOptions({ database });
const client = new PgpmMigrate(pgEnv);

let selectedPackage: string;

if (argv.package) {
selectedPackage = argv.package;
selectedPackage = resolvePackageAlias(argv.package as string, cwd);
} else {
const packageStatuses = await client.status();

Expand Down Expand Up @@ -66,10 +69,11 @@ export async function selectDeployedPackage(
argv: Partial<Record<string, any>>,
prompter: Inquirerer,
log: Logger,
action: 'revert' | 'verify' = 'revert'
action: 'revert' | 'verify' = 'revert',
cwd: string = process.cwd()
): Promise<string | undefined> {
if (argv.package) {
return argv.package;
return resolvePackageAlias(argv.package as string, cwd);
}

const pgEnv = getPgEnvOptions({ database });
Expand Down
1 change: 1 addition & 0 deletions packages/pgpm/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export * from './cli-error';
export * from './deployed-changes';
export * from './module-utils';
export * from './npm-version';
export * from './package-alias';
export * from './update-check';
export * from './update-config';
5 changes: 4 additions & 1 deletion packages/pgpm/src/utils/module-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { errors } from '@pgpmjs/types';
import { Inquirerer } from 'inquirerer';
import { ParsedArgs } from 'minimist';

import { resolvePackageAlias } from './package-alias';

/**
* Handle package selection for operations that need a specific package
* Returns the selected package name, or undefined if validation fails or no packages exist
Expand Down Expand Up @@ -33,7 +35,8 @@ export async function selectPackage(

// If a specific package was provided, validate it
if (argv.package) {
const packageName = argv.package as string;
const inputPackage = argv.package as string;
const packageName = resolvePackageAlias(inputPackage, cwd);
if (log) log.info(`Using specified package: ${packageName}`);

if (!moduleNames.includes(packageName)) {
Expand Down
104 changes: 104 additions & 0 deletions packages/pgpm/src/utils/package-alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { PgpmPackage } from '@pgpmjs/core';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

export interface PackageAliasMap {
[npmName: string]: string;
}

/**
* Build a map of npm package names to control file names (extension names).
* This allows users to reference packages by their npm name (e.g., @scope/my-module)
* instead of the control file name (e.g., my-module).
*/
export function buildPackageAliasMap(cwd: string): PackageAliasMap {
const aliasMap: PackageAliasMap = {};

try {
const pkg = new PgpmPackage(cwd);
const workspacePath = pkg.getWorkspacePath();

if (!workspacePath) {
return aliasMap;
}

const modules = pkg.getModuleMap();

for (const [controlName, moduleInfo] of Object.entries(modules)) {
const modulePath = join(workspacePath, moduleInfo.path);
const packageJsonPath = join(modulePath, 'package.json');

if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const npmName = packageJson.name;

if (npmName && npmName !== controlName) {
aliasMap[npmName] = controlName;
}
} catch {
// Skip modules with invalid package.json
}
}
}
} catch {
// Return empty map if we can't access workspace
}

return aliasMap;
}

/**
* Resolve a package name that might be an npm alias to its control file name.
* If the input is already a control file name or not found in aliases, returns as-is.
*
* @param input - The package name (could be npm name like @scope/pkg or control name)
* @param cwd - The current working directory
* @returns The resolved control file name
*/
export function resolvePackageAlias(input: string, cwd: string): string {
if (!input) {
return input;
}

const aliasMap = buildPackageAliasMap(cwd);
return aliasMap[input] ?? input;
}

/**
* Get the npm package name for a given control file name, if available.
* Returns undefined if no npm alias exists.
*
* @param controlName - The control file name (extension name)
* @param cwd - The current working directory
* @returns The npm package name or undefined
*/
export function getNpmNameForControl(controlName: string, cwd: string): string | undefined {
const aliasMap = buildPackageAliasMap(cwd);

for (const [npmName, ctrlName] of Object.entries(aliasMap)) {
if (ctrlName === controlName) {
return npmName;
}
}

return undefined;
}

/**
* Format a module name for display, showing both control name and npm alias if available.
* Example: "my-module (@scope/my-module)" or just "my-module" if no alias
*
* @param controlName - The control file name
* @param cwd - The current working directory
* @returns Formatted display string
*/
export function formatModuleNameWithAlias(controlName: string, cwd: string): string {
const npmName = getNpmNameForControl(controlName, cwd);

if (npmName) {
return `${controlName} (${npmName})`;
}

return controlName;
}