Skip to content

Commit e28c455

Browse files
authored
Merge pull request #402 from constructive-io/feat/aliases
Feat/aliases
2 parents 293ffe3 + 52d8397 commit e28c455

9 files changed

Lines changed: 236 additions & 21 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { teardownPgPools } from 'pg-cache';
4+
5+
import { CLIDeployTestFixture } from '../test-utils';
6+
7+
jest.setTimeout(30000);
8+
9+
describe('CLI Package Alias Resolution', () => {
10+
let fixture: CLIDeployTestFixture;
11+
let testDb: any;
12+
13+
beforeAll(async () => {
14+
fixture = new CLIDeployTestFixture('sqitch', 'simple-w-tags');
15+
16+
// Modify the package.json of my-first to have a scoped npm name
17+
// This simulates the case where package.json name differs from control file name
18+
const packageJsonPath = fixture.fixturePath('packages', 'my-first', 'package.json');
19+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
20+
packageJson.name = '@test-scope/my-first';
21+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
22+
});
23+
24+
beforeEach(async () => {
25+
testDb = await fixture.setupTestDatabase();
26+
});
27+
28+
afterAll(async () => {
29+
await fixture.cleanup();
30+
await teardownPgPools();
31+
});
32+
33+
it('should deploy using npm package name alias instead of control file name', async () => {
34+
// Deploy using the scoped npm name (@test-scope/my-first) instead of control file name (my-first)
35+
const commands = `lql deploy --database ${testDb.name} --package @test-scope/my-first --yes`;
36+
37+
await fixture.runTerminalCommands(commands, {
38+
database: testDb.name
39+
}, true);
40+
41+
// Verify deployment succeeded - the schema should exist
42+
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);
43+
44+
// Verify the deployed changes are recorded under the control file name (my-first), not the npm name
45+
const deployedChanges = await testDb.getDeployedChanges();
46+
expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true);
47+
});
48+
49+
it('should still work with control file name directly (backward compatibility)', async () => {
50+
// Deploy using the control file name directly
51+
const commands = `lql deploy --database ${testDb.name} --package my-first --yes`;
52+
53+
await fixture.runTerminalCommands(commands, {
54+
database: testDb.name
55+
}, true);
56+
57+
// Verify deployment succeeded
58+
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);
59+
60+
const deployedChanges = await testDb.getDeployedChanges();
61+
expect(deployedChanges.some((change: any) => change.package === 'my-first')).toBe(true);
62+
});
63+
64+
it('should deploy to specific change using npm package name alias', async () => {
65+
// Deploy to a specific change using the aliased npm name
66+
const commands = `lql deploy --database ${testDb.name} --package @test-scope/my-first --to schema_myfirstapp --yes`;
67+
68+
await fixture.runTerminalCommands(commands, {
69+
database: testDb.name
70+
}, true);
71+
72+
// Verify only the schema was deployed (not the tables)
73+
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);
74+
75+
const deployedChanges = await testDb.getDeployedChanges();
76+
expect(deployedChanges.find((change: any) =>
77+
change.package === 'my-first' && change.change_name === 'schema_myfirstapp'
78+
)).toBeTruthy();
79+
});
80+
81+
it('should revert using npm package name alias', async () => {
82+
// First deploy
83+
const deployCommands = `lql deploy --database ${testDb.name} --package @test-scope/my-first --yes`;
84+
await fixture.runTerminalCommands(deployCommands, {
85+
database: testDb.name
86+
}, true);
87+
88+
expect(await testDb.exists('schema', 'myfirstapp')).toBe(true);
89+
90+
// Then revert using the aliased npm name
91+
const revertCommands = `lql revert --database ${testDb.name} --package @test-scope/my-first --yes`;
92+
await fixture.runTerminalCommands(revertCommands, {
93+
database: testDb.name
94+
}, true);
95+
96+
// Verify revert succeeded
97+
expect(await testDb.exists('schema', 'myfirstapp')).toBe(false);
98+
});
99+
});

packages/pgpm/src/commands/deploy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
getSpawnEnvWithPg,
1010
} from 'pg-env';
1111

12-
import { getTargetDatabase } from '../utils';
12+
import { getTargetDatabase, resolvePackageAlias } from '../utils';
1313
import { selectPackage } from '../utils/module-utils';
1414

1515
const deployUsageText = `
@@ -154,9 +154,10 @@ export default async (
154154
} else if (packageName) {
155155
target = packageName;
156156
} else if (argv.package && argv.to) {
157-
target = `${argv.package}:${argv.to}`;
157+
const resolvedPackage = resolvePackageAlias(argv.package as string, cwd);
158+
target = `${resolvedPackage}:${argv.to}`;
158159
} else if (argv.package) {
159-
target = argv.package as string;
160+
target = resolvePackageAlias(argv.package as string, cwd);
160161
}
161162

162163
await project.deploy(

packages/pgpm/src/commands/revert.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Logger } from '@pgpmjs/logger';
44
import { CLIOptions, Inquirerer, Question } from 'inquirerer';
55
import { getPgEnvOptions } from 'pg-env';
66

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

@@ -84,7 +84,7 @@ export default async (
8484

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

104104
if (argv.to === true) {
105-
target = await selectDeployedChange(database, argv, prompter, log, 'revert');
105+
target = await selectDeployedChange(database, argv, prompter, log, 'revert', cwd);
106106
if (!target) {
107107
await cliExitWithError('No target selected, operation cancelled');
108108
}
109-
} else if (packageName && argv.to) {
109+
}else if (packageName && argv.to) {
110110
target = `${packageName}:${argv.to}`;
111111
} else if (packageName) {
112112
target = packageName;
113113
} else if (argv.package && argv.to) {
114-
target = `${argv.package}:${argv.to}`;
114+
const resolvedPackage = resolvePackageAlias(argv.package as string, cwd);
115+
target = `${resolvedPackage}:${argv.to}`;
115116
} else if (argv.package) {
116-
target = argv.package as string;
117+
target = resolvePackageAlias(argv.package as string, cwd);
117118
}
118119

119120
await pkg.revert(

packages/pgpm/src/commands/tag.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path';
66

77
import { extractFirst } from '../utils/argv';
88
import { selectPackage } from '../utils/module-utils';
9+
import { resolvePackageAlias } from '../utils/package-alias';
910

1011
const log = new Logger('tag');
1112

@@ -61,7 +62,7 @@ export default async (
6162
let packageName: string | undefined;
6263

6364
if (argv.package) {
64-
packageName = argv.package as string;
65+
packageName = resolvePackageAlias(argv.package as string, cwd);
6566
log.info(`Using specified package: ${packageName}`);
6667
}
6768
else if (pkg.isInModule()) {

packages/pgpm/src/commands/verify.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Logger } from '@pgpmjs/logger';
44
import { CLIOptions, Inquirerer, Question } from 'inquirerer';
55
import { getPgEnvOptions } from 'pg-env';
66

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

@@ -62,7 +62,7 @@ export default async (
6262

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

7979
if (argv.to === true) {
80-
target = await selectDeployedChange(database, argv, prompter, log, 'verify');
80+
target = await selectDeployedChange(database, argv, prompter, log, 'verify', cwd);
8181
if (!target) {
8282
await cliExitWithError('No target selected, operation cancelled');
8383
}
84-
} else if (packageName && argv.to) {
84+
}else if (packageName && argv.to) {
8585
target = `${packageName}:${argv.to}`;
8686
} else if (packageName) {
8787
target = packageName;
8888
} else if (argv.package && argv.to) {
89-
target = `${argv.package}:${argv.to}`;
89+
const resolvedPackage = resolvePackageAlias(argv.package as string, cwd);
90+
target = `${resolvedPackage}:${argv.to}`;
9091
} else if (argv.package) {
91-
target = argv.package as string;
92+
target = resolvePackageAlias(argv.package as string, cwd);
9293
}
9394

9495
await project.verify(

packages/pgpm/src/utils/deployed-changes.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,23 @@ import { Logger } from '@pgpmjs/logger';
33
import { Inquirerer } from 'inquirerer';
44
import { getPgEnvOptions } from 'pg-env';
55

6+
import { resolvePackageAlias } from './package-alias';
7+
68
export async function selectDeployedChange(
79
database: string,
810
argv: Partial<Record<string, any>>,
911
prompter: Inquirerer,
1012
log: Logger,
11-
action: 'revert' | 'verify' = 'revert'
13+
action: 'revert' | 'verify' = 'revert',
14+
cwd: string = process.cwd()
1215
): Promise<string | undefined> {
1316
const pgEnv = getPgEnvOptions({ database });
1417
const client = new PgpmMigrate(pgEnv);
1518

1619
let selectedPackage: string;
1720

1821
if (argv.package) {
19-
selectedPackage = argv.package;
22+
selectedPackage = resolvePackageAlias(argv.package as string, cwd);
2023
} else {
2124
const packageStatuses = await client.status();
2225

@@ -66,10 +69,11 @@ export async function selectDeployedPackage(
6669
argv: Partial<Record<string, any>>,
6770
prompter: Inquirerer,
6871
log: Logger,
69-
action: 'revert' | 'verify' = 'revert'
72+
action: 'revert' | 'verify' = 'revert',
73+
cwd: string = process.cwd()
7074
): Promise<string | undefined> {
7175
if (argv.package) {
72-
return argv.package;
76+
return resolvePackageAlias(argv.package as string, cwd);
7377
}
7478

7579
const pgEnv = getPgEnvOptions({ database });

packages/pgpm/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export * from './cli-error';
55
export * from './deployed-changes';
66
export * from './module-utils';
77
export * from './npm-version';
8+
export * from './package-alias';
89
export * from './update-check';
910
export * from './update-config';

packages/pgpm/src/utils/module-utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { errors } from '@pgpmjs/types';
44
import { Inquirerer } from 'inquirerer';
55
import { ParsedArgs } from 'minimist';
66

7+
import { resolvePackageAlias } from './package-alias';
8+
79
/**
810
* Handle package selection for operations that need a specific package
911
* Returns the selected package name, or undefined if validation fails or no packages exist
@@ -33,7 +35,8 @@ export async function selectPackage(
3335

3436
// If a specific package was provided, validate it
3537
if (argv.package) {
36-
const packageName = argv.package as string;
38+
const inputPackage = argv.package as string;
39+
const packageName = resolvePackageAlias(inputPackage, cwd);
3740
if (log) log.info(`Using specified package: ${packageName}`);
3841

3942
if (!moduleNames.includes(packageName)) {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { PgpmPackage } from '@pgpmjs/core';
2+
import { existsSync, readFileSync } from 'fs';
3+
import { join } from 'path';
4+
5+
export interface PackageAliasMap {
6+
[npmName: string]: string;
7+
}
8+
9+
/**
10+
* Build a map of npm package names to control file names (extension names).
11+
* This allows users to reference packages by their npm name (e.g., @scope/my-module)
12+
* instead of the control file name (e.g., my-module).
13+
*/
14+
export function buildPackageAliasMap(cwd: string): PackageAliasMap {
15+
const aliasMap: PackageAliasMap = {};
16+
17+
try {
18+
const pkg = new PgpmPackage(cwd);
19+
const workspacePath = pkg.getWorkspacePath();
20+
21+
if (!workspacePath) {
22+
return aliasMap;
23+
}
24+
25+
const modules = pkg.getModuleMap();
26+
27+
for (const [controlName, moduleInfo] of Object.entries(modules)) {
28+
const modulePath = join(workspacePath, moduleInfo.path);
29+
const packageJsonPath = join(modulePath, 'package.json');
30+
31+
if (existsSync(packageJsonPath)) {
32+
try {
33+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
34+
const npmName = packageJson.name;
35+
36+
if (npmName && npmName !== controlName) {
37+
aliasMap[npmName] = controlName;
38+
}
39+
} catch {
40+
// Skip modules with invalid package.json
41+
}
42+
}
43+
}
44+
} catch {
45+
// Return empty map if we can't access workspace
46+
}
47+
48+
return aliasMap;
49+
}
50+
51+
/**
52+
* Resolve a package name that might be an npm alias to its control file name.
53+
* If the input is already a control file name or not found in aliases, returns as-is.
54+
*
55+
* @param input - The package name (could be npm name like @scope/pkg or control name)
56+
* @param cwd - The current working directory
57+
* @returns The resolved control file name
58+
*/
59+
export function resolvePackageAlias(input: string, cwd: string): string {
60+
if (!input) {
61+
return input;
62+
}
63+
64+
const aliasMap = buildPackageAliasMap(cwd);
65+
return aliasMap[input] ?? input;
66+
}
67+
68+
/**
69+
* Get the npm package name for a given control file name, if available.
70+
* Returns undefined if no npm alias exists.
71+
*
72+
* @param controlName - The control file name (extension name)
73+
* @param cwd - The current working directory
74+
* @returns The npm package name or undefined
75+
*/
76+
export function getNpmNameForControl(controlName: string, cwd: string): string | undefined {
77+
const aliasMap = buildPackageAliasMap(cwd);
78+
79+
for (const [npmName, ctrlName] of Object.entries(aliasMap)) {
80+
if (ctrlName === controlName) {
81+
return npmName;
82+
}
83+
}
84+
85+
return undefined;
86+
}
87+
88+
/**
89+
* Format a module name for display, showing both control name and npm alias if available.
90+
* Example: "my-module (@scope/my-module)" or just "my-module" if no alias
91+
*
92+
* @param controlName - The control file name
93+
* @param cwd - The current working directory
94+
* @returns Formatted display string
95+
*/
96+
export function formatModuleNameWithAlias(controlName: string, cwd: string): string {
97+
const npmName = getNpmNameForControl(controlName, cwd);
98+
99+
if (npmName) {
100+
return `${controlName} (${npmName})`;
101+
}
102+
103+
return controlName;
104+
}

0 commit comments

Comments
 (0)