Skip to content

Commit ff83a2f

Browse files
committed
feat: Modified the destroy flow to support asking users for identifying parameters
1 parent a817729 commit ff83a2f

File tree

4 files changed

+144
-68
lines changed

4 files changed

+144
-68
lines changed

src/commands/destroy.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,12 @@ For more information, visit: https://docs.codifycli.com/commands/destory`
4242
.filter((r) => r.type === 'arg')
4343
.map((r) => r.input);
4444

45-
if (args.length === 0) {
46-
throw new Error('At least one resource <type> must be specified. Ex: "codify destroy homebrew"')
47-
}
48-
4945
if (flags.path) {
5046
this.log(`Applying Codify from: ${flags.path}`);
5147
}
5248

5349
await DestroyOrchestrator.run({
54-
ids: args,
50+
typeIds: args,
5551
path: flags.path,
5652
}, this.reporter)
5753

src/orchestrators/destroy.ts

Lines changed: 137 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,42 @@
1-
import { InternalError } from '../common/errors.js';
2-
import { PluginInitOrchestrator } from '../common/initialize-plugins.js';
1+
import { InitializationResult, PluginInitOrchestrator } from '../common/initialize-plugins.js';
2+
import { Plan } from '../entities/plan.js';
33
import { Project } from '../entities/project.js';
44
import { ResourceConfig } from '../entities/resource-config.js';
5+
import { ResourceInfo } from '../entities/resource-info.js';
56
import { ProcessName, SubProcessName, ctx } from '../events/context.js';
67
import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js';
7-
import { Reporter } from '../ui/reporters/reporter.js';
8-
import { getTypeAndNameFromId } from '../utils/index.js';
8+
import { PromptType, Reporter } from '../ui/reporters/reporter.js';
9+
import { wildCardMatch } from '../utils/wild-card-match.js';
910

1011
export interface DestroyArgs {
11-
ids: string[];
12+
typeIds: string[];
1213
path?: string;
1314
secureMode?: boolean;
1415
}
1516

1617
export class DestroyOrchestrator {
1718

1819
static async run(args: DestroyArgs, reporter: Reporter) {
19-
const { ids } = args;
20-
if (ids.length === 0) {
21-
throw new InternalError('getDestroyPlan called with no ids passed in');
22-
}
23-
20+
const typeIds = args.typeIds?.filter(Boolean)
2421
ctx.processStarted(ProcessName.DESTROY)
25-
26-
const { typeIdsToDependenciesMap, pluginManager, project } = await PluginInitOrchestrator.run({
27-
...args,
28-
allowEmptyProject: true,
29-
transformProject(project) {
30-
project.filter(ids) // We only care about the types being uninstalled
31-
32-
const nonProjectConfigs = ids.filter((id) =>
33-
project.resourceConfigs.findIndex((r) => r.id.includes(id)) === -1
34-
)
35-
36-
project.add(...nonProjectConfigs.map((id) => {
37-
const { name, type } = getTypeAndNameFromId(id);
38-
return new ResourceConfig({ name, type })
39-
}))
40-
41-
return project;
42-
}
43-
}, reporter);
4422

45-
await DestroyOrchestrator.validate(project, pluginManager, typeIdsToDependenciesMap)
23+
const initializationResult = await PluginInitOrchestrator.run(
24+
{ ...args, allowEmptyProject: true, },
25+
reporter
26+
);
27+
const { pluginManager, project } = initializationResult;
4628

47-
const uninstallProject = project.toDestroyProject()
48-
uninstallProject.resolveDependenciesAndCalculateEvalOrder(typeIdsToDependenciesMap);
29+
if ((!typeIds || typeIds.length === 0) && project.isEmpty()) {
30+
throw new Error('At least one resource [type] must be specified. Ex: "codify destroy homebrew". Or the destroy command must be run in a directory with a valid codify file')
31+
}
32+
33+
const { plan, destroyProject } = (!typeIds || typeIds.length === 0)
34+
? await DestroyOrchestrator.destroyExistingProject(reporter, initializationResult)
35+
: await DestroyOrchestrator.destroySpecificResources(typeIds, reporter, initializationResult)
4936

50-
const plan = await ctx.subprocess(ProcessName.PLAN, () =>
51-
pluginManager.plan(uninstallProject)
52-
)
5337

5438
plan.sortByEvalOrder(project.evaluationOrder);
55-
uninstallProject.removeNoopFromEvaluationOrder(plan);
39+
destroyProject.removeNoopFromEvaluationOrder(plan);
5640

5741
reporter.displayPlan(plan);
5842

@@ -70,21 +54,132 @@ export class DestroyOrchestrator {
7054
const filteredPlan = plan.filterNoopResources()
7155

7256
await ctx.process(ProcessName.DESTROY, () =>
73-
pluginManager.apply(uninstallProject, filteredPlan)
57+
pluginManager.apply(destroyProject, filteredPlan)
7458
)
7559

7660
await reporter.displayMessage(`
7761
🎉 Finished applying 🎉
7862
Open a new terminal or source '.zshrc' for the new changes to be reflected`);
7963
}
8064

81-
private static async validate(project: Project, pluginManager: PluginManager, dependencyMap: DependencyMap): Promise<void> {
82-
ctx.subprocessStarted(SubProcessName.VALIDATE)
65+
/** This method is responsible for generating a plan for specific resources specified by the user */
66+
private static async destroySpecificResources(
67+
typeIds: string[],
68+
reporter: Reporter,
69+
initializeResult: InitializationResult
70+
): Promise<{ plan: Plan, destroyProject: Project }> {
71+
const { project, pluginManager, typeIdsToDependenciesMap } = initializeResult;
72+
73+
const matchedTypes = this.matchTypeIds(typeIds, [...typeIdsToDependenciesMap.keys()])
74+
await DestroyOrchestrator.validateTypeIds(matchedTypes, project, pluginManager, typeIdsToDependenciesMap);
75+
76+
const resourceInfoList = (await pluginManager.getMultipleResourceInfo(matchedTypes));
77+
const resourcesToDestroy = await DestroyOrchestrator.getDestroyParameters(reporter, project, resourceInfoList);
78+
79+
const destroyProject = new Project(
80+
null,
81+
resourcesToDestroy,
82+
project.codifyFiles
83+
).toDestroyProject();
84+
85+
destroyProject.resolveDependenciesAndCalculateEvalOrder(typeIdsToDependenciesMap);
86+
const plan = await ctx.subprocess(ProcessName.PLAN, () =>
87+
pluginManager.plan(destroyProject)
88+
)
89+
90+
return { plan, destroyProject };
91+
}
92+
93+
/** This method is responsible for generating the plan when no args are specified (ie: destroy all resources inside a codify.json file) **/
94+
private static async destroyExistingProject(
95+
reporter: Reporter,
96+
initializeResult: InitializationResult
97+
): Promise<{ plan: Plan, destroyProject: Project }> {
98+
const { pluginManager, project, typeIdsToDependenciesMap } = initializeResult;
99+
100+
await ctx.subprocess(SubProcessName.VALIDATE, async () => {
101+
project.validateTypeIds(typeIdsToDependenciesMap);
102+
const validationResults = await pluginManager.validate(project);
103+
project.handlePluginResourceValidationResults(validationResults);
104+
})
105+
106+
const destroyProject = project.toDestroyProject();
107+
destroyProject.resolveDependenciesAndCalculateEvalOrder(typeIdsToDependenciesMap);
108+
109+
const plan = await ctx.subprocess(ProcessName.PLAN, () =>
110+
pluginManager.plan(destroyProject)
111+
)
112+
113+
return { plan, destroyProject };
114+
}
115+
116+
private static matchTypeIds(typeIds: string[], validTypeIds: string[]): string[] {
117+
const result: string[] = [];
118+
const unsupportedTypeIds: string[] = [];
119+
120+
for (const typeId of typeIds) {
121+
if (!typeId.includes('*') && !typeId.includes('?')) {
122+
const matched = validTypeIds.includes(typeId);
123+
if (!matched) {
124+
unsupportedTypeIds.push(typeId);
125+
continue;
126+
}
127+
128+
result.push(typeId)
129+
continue;
130+
}
131+
132+
const matched = validTypeIds.filter((valid) => wildCardMatch(valid, typeId))
133+
if (matched.length === 0) {
134+
unsupportedTypeIds.push(typeId);
135+
continue;
136+
}
137+
138+
result.push(...matched);
139+
}
140+
141+
if (unsupportedTypeIds.length > 0) {
142+
throw new Error(`The following resources cannot be imported. No plugins found that support the following types:
143+
${JSON.stringify(unsupportedTypeIds)}`);
144+
}
145+
146+
return result;
147+
}
83148

149+
private static async validateTypeIds(typeIds: string[], project: Project, pluginManager: PluginManager, dependencyMap: DependencyMap): Promise<void> {
84150
project.validateTypeIds(dependencyMap);
85-
const validationResults = await pluginManager.validate(project);
86-
project.handlePluginResourceValidationResults(validationResults);
87151

88-
ctx.subprocessFinished(SubProcessName.VALIDATE)
152+
const unsupportedTypeIds = typeIds.filter((type) => !dependencyMap.has(type));
153+
if (unsupportedTypeIds.length > 0) {
154+
throw new Error(`The following resources cannot be destroyed. No plugins found that support the following types:
155+
${JSON.stringify(unsupportedTypeIds)}`);
156+
}
89157
}
158+
159+
private static async getDestroyParameters(reporter: Reporter, project: Project, resourceInfoList: ResourceInfo[]): Promise<Array<ResourceConfig>> {
160+
// Figure out which resources we need to prompt the user for additional info (based on the resource info)
161+
const [noPrompt, askPrompt] = resourceInfoList.reduce((result, info) => {
162+
info.getRequiredParameters().length === 0 ? result[0].push(info) : result[1].push(info);
163+
return result;
164+
}, [<ResourceInfo[]>[], <ResourceInfo[]>[]])
165+
166+
askPrompt.forEach((info) => {
167+
const matchedResources = project.findAll(info.type);
168+
if (matchedResources.length > 0) {
169+
info.attachDefaultValues(matchedResources[0]);
170+
}
171+
})
172+
173+
if (askPrompt.length > 0) {
174+
await reporter.displayImportWarning(askPrompt.map((r) => r.type), noPrompt.map((r) => r.type));
175+
}
176+
177+
const userSupplied = await reporter.promptUserForValues(askPrompt, PromptType.DESTROY);
178+
179+
return [
180+
...noPrompt.map((info) => new ResourceConfig({ type: info.type })),
181+
...userSupplied
182+
]
183+
}
184+
90185
}

src/orchestrators/import.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,6 @@ export interface ImportArgs {
2222
secureMode?: boolean;
2323
}
2424

25-
export interface RequiredParameter {
26-
/**
27-
* The name of the parameter.
28-
*/
29-
name: string;
30-
31-
/**
32-
* The type (string, number, boolean) of the parameter. Un-related to type ids
33-
*/
34-
type: string;
35-
36-
/**
37-
* Description for a field
38-
*/
39-
description?: string;
40-
}
41-
4225
export class ImportOrchestrator {
4326
static async run(
4427
args: ImportArgs,
@@ -57,7 +40,9 @@ export class ImportOrchestrator {
5740
throw new Error('At least one resource [type] must be specified. Ex: "codify import homebrew". Or the import command must be run in a directory with a valid codify file')
5841
}
5942

60-
await (!typeIds || typeIds.length === 0 ? ImportOrchestrator.runExistingProject(reporter, initializationResult) : ImportOrchestrator.runNewImport(typeIds, reporter, initializationResult));
43+
await (!typeIds || typeIds.length === 0
44+
? ImportOrchestrator.runExistingProject(reporter, initializationResult)
45+
: ImportOrchestrator.runNewImport(typeIds, reporter, initializationResult));
6146
}
6247

6348
/** Import new resources. Type ids supplied. This will ask for any required parameters */

test/orchestrator/destroy/destroy.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('Destroy orchestrator tests', () => {
5252
expect(MockOs.get('mock')).to.toMatchObject({ propA: 'current' })
5353

5454
await DestroyOrchestrator.run({
55-
ids: ['mock'],
55+
typeIds: ['mock'],
5656
path: path.join(__dirname, 'simple.codify.json')
5757
}, reporter)
5858

@@ -72,7 +72,7 @@ describe('Destroy orchestrator tests', () => {
7272
});
7373

7474
await DestroyOrchestrator.run({
75-
ids: ['mock'],
75+
typeIds: ['mock'],
7676
path: path.join(__dirname, 'codify.json')
7777
}, reporter)
7878

@@ -99,7 +99,7 @@ describe('Destroy orchestrator tests', () => {
9999
expect(MockOs.get('mock')).to.toMatchObject({ propA: 'current' })
100100

101101
await DestroyOrchestrator.run({
102-
ids: ['mock.0'],
102+
typeIds: ['mock.0'],
103103
path: path.join(__dirname, 'codify.json')
104104
}, reporter)
105105

0 commit comments

Comments
 (0)