Skip to content

Commit 95f23e9

Browse files
committed
feat: Moved resource matching to the plugin for more accurate matching
1 parent 2337888 commit 95f23e9

File tree

8 files changed

+99
-155
lines changed

8 files changed

+99
-155
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"ajv": "^8.12.0",
1414
"ajv-formats": "^3.0.1",
1515
"chalk": "^5.3.0",
16-
"codify-schemas": "^1.0.70",
16+
"codify-schemas": "^1.0.73",
1717
"debug": "^4.3.4",
1818
"detect-indent": "^7.0.1",
1919
"diff": "^7.0.0",

src/entities/resource-config.test.ts

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -24,104 +24,4 @@ describe('Resource config unit tests', () => {
2424

2525
it('plugin versions must be semvers', () => {
2626
})
27-
28-
it ('detects if two resource configs represent the same thing on the system (different types)', () => {
29-
const resource1 = new ResourceConfig({
30-
type: 'type1',
31-
})
32-
const resource2 = new ResourceConfig({
33-
type: 'type2',
34-
})
35-
expect(resource1.isSameOnSystem(resource2, false)).to.be.false;
36-
37-
const resource3 = new ResourceConfig({
38-
type: 'type1',
39-
})
40-
const resource4 = new ResourceConfig({
41-
type: 'type1',
42-
})
43-
expect(resource3.isSameOnSystem(resource4, false)).to.be.true;
44-
})
45-
46-
47-
it ('detects if two resource configs represent the same thing on the system (different names)', () => {
48-
// Fails
49-
const resource1 = new ResourceConfig({
50-
type: 'type1',
51-
name: 'name1',
52-
})
53-
const resource2 = new ResourceConfig({
54-
type: 'type1',
55-
name: 'name2'
56-
})
57-
expect(resource1.isSameOnSystem(resource2)).to.be.false;
58-
59-
// Passes
60-
const resource3 = new ResourceConfig({
61-
type: 'type1',
62-
name: 'name1',
63-
})
64-
const resource4 = new ResourceConfig({
65-
type: 'type1',
66-
name: 'name1'
67-
})
68-
expect(resource3.isSameOnSystem(resource4, false)).to.be.true;
69-
})
70-
71-
it ('detects if two resource configs represent the same thing on the system (different required parameters)', () => {
72-
// Passes
73-
const resourceInfo = ResourceInfo.fromResponseData({
74-
type: 'type1',
75-
schema: {
76-
type: 'object',
77-
required: ['param1', 'param2'],
78-
properties: {
79-
param1: {},
80-
param2: {},
81-
param3: {}
82-
}
83-
}
84-
});
85-
86-
const resource1 = new ResourceConfig({
87-
type: 'type1',
88-
param2: 'b',
89-
name: 'name1',
90-
param1: 'a',
91-
param3: 'c'
92-
})
93-
resource1.attachResourceInfo(resourceInfo)
94-
95-
const resource2 = new ResourceConfig({
96-
param3: 'different',
97-
type: 'type1',
98-
name: 'name1',
99-
param1: 'a',
100-
param2: 'b',
101-
})
102-
resource2.attachResourceInfo(resourceInfo)
103-
104-
expect(resource1.isSameOnSystem(resource2)).to.be.true;
105-
106-
// Fails
107-
const resource3 = new ResourceConfig({
108-
type: 'type1',
109-
name: 'name1',
110-
param1: 'a',
111-
param2: 'b',
112-
param3: 'c'
113-
})
114-
resource3.attachResourceInfo(resourceInfo)
115-
116-
const resource4 = new ResourceConfig({
117-
type: 'type1',
118-
name: 'name1',
119-
param1: 'a',
120-
param2: 'different',
121-
param3: 'different'
122-
})
123-
resource4.attachResourceInfo(resourceInfo)
124-
125-
expect(resource3.isSameOnSystem(resource4)).to.be.false;
126-
})
12727
});

src/entities/resource-config.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -81,43 +81,13 @@ export class ResourceConfig implements ConfigBlock {
8181
return externalId === this.id;
8282
}
8383

84-
/**
85-
* Useful for imports, creates and destroys. This checks if two resources represents the same installation on the system.
86-
*/
87-
isSameOnSystem(other: ResourceConfig, checkResourceInfo = true): boolean {
88-
if (other.type !== this.type) {
84+
isDeepEqual(other?: ResourceConfig | null): boolean {
85+
if (!other) {
8986
return false;
9087
}
9188

92-
// If names are specified then that means Codify intends for the resources to be the same
93-
if (other.name && this.name && other.name !== this.name) {
94-
return false;
95-
}
96-
97-
if (!checkResourceInfo) {
98-
return true;
99-
}
100-
101-
if (!this.resourceInfo || !other.resourceInfo) {
102-
throw new Error(`checkResourceInfo specified but no resource info provided (${this.type}) (other: ${other.type})`)
103-
}
104-
105-
if (!this.resourceInfo.allowMultiple || !other.resourceInfo.allowMultiple) {
106-
return true;
107-
}
108-
109-
const thisRequiredKeys = new Set(this.resourceInfo.allowMultiple.identifyingParameters);
110-
const otherRequiredKeys = new Set(other.resourceInfo.allowMultiple.identifyingParameters);
111-
112-
const thisRequiredParameters = Object.fromEntries(Object.entries(this.parameters)
113-
.filter(([k]) => thisRequiredKeys.has(k))
114-
);
115-
const otherRequiredParameters = Object.fromEntries(Object.entries(other.parameters)
116-
.filter(([k]) => otherRequiredKeys.has(k))
117-
);
118-
119-
return deepEqual(thisRequiredParameters, otherRequiredParameters);
120-
89+
return deepEqual(other.parameters, this.parameters)
90+
&& deepEqual({ type: this.type, name: this.name }, { type: other.type, name: other.name });
12191
}
12292

12393
setName(name: string) {

src/orchestrators/import.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class ImportOrchestrator {
8080
resourceInfoList.push(...(await pluginManager.getMultipleResourceInfo(
8181
project.resourceConfigs.map((r) => r.type)
8282
)));
83-
await ImportOrchestrator.saveResults(reporter, importResult, project, resourceInfoList)
83+
await ImportOrchestrator.saveResults(reporter, importResult, project, resourceInfoList, pluginManager)
8484
}
8585

8686
/** Update an existing project. This will use the existing resources as the parameters (no user input required). */
@@ -104,6 +104,7 @@ export class ImportOrchestrator {
104104
importResult,
105105
resourceInfoList,
106106
project.codifyFiles[0],
107+
pluginManager,
107108
);
108109
}
109110

@@ -147,28 +148,28 @@ export class ImportOrchestrator {
147148
private static matchTypeIds(typeIds: string[], validTypeIds: string[]): string[] {
148149
const result: string[] = [];
149150
const unsupportedTypeIds: string[] = [];
150-
151+
151152
for (const typeId of typeIds) {
152153
if (!typeId.includes('*') && !typeId.includes('?')) {
153154
const matched = validTypeIds.includes(typeId);
154155
if (!matched) {
155156
unsupportedTypeIds.push(typeId);
156157
continue;
157158
}
158-
159+
159160
result.push(typeId)
160161
continue;
161162
}
162-
163+
163164
const matched = validTypeIds.filter((valid) => wildCardMatch(valid, typeId))
164165
if (matched.length === 0) {
165166
unsupportedTypeIds.push(typeId);
166167
continue;
167168
}
168-
169+
169170
result.push(...matched);
170171
}
171-
172+
172173
if (unsupportedTypeIds.length > 0) {
173174
throw new Error(`The following resources cannot be imported. No plugins found that support the following types:
174175
${JSON.stringify(unsupportedTypeIds)}`);
@@ -213,7 +214,13 @@ ${JSON.stringify(unsupportedTypeIds)}`);
213214
]
214215
}
215216

216-
private static async saveResults(reporter: Reporter, importResult: ImportResult, project: Project, resourceInfoList: ResourceInfo[]): Promise<void> {
217+
private static async saveResults(
218+
reporter: Reporter,
219+
importResult: ImportResult,
220+
project: Project,
221+
resourceInfoList: ResourceInfo[],
222+
pluginManager: PluginManager,
223+
): Promise<void> {
217224
const projectExists = !project.isEmpty();
218225
const multipleCodifyFiles = project.codifyFiles.length > 1;
219226

@@ -233,7 +240,7 @@ ${JSON.stringify(unsupportedTypeIds)}`);
233240
const file = multipleCodifyFiles
234241
? project.codifyFiles[await reporter.promptOptions('\nIf new resources are added, where to write them?', project.codifyFiles)]
235242
: project.codifyFiles[0];
236-
await ImportOrchestrator.updateExistingFiles(reporter, project, importResult, resourceInfoList, file);
243+
await ImportOrchestrator.updateExistingFiles(reporter, project, importResult, resourceInfoList, file, pluginManager);
237244
return;
238245
}
239246

@@ -257,6 +264,7 @@ ${JSON.stringify(unsupportedTypeIds)}`);
257264
importResult: ImportResult,
258265
resourceInfoList: ResourceInfo[],
259266
preferredFile: string, // File to write any new resources (unknown file path)
267+
pluginManager: PluginManager,
260268
): Promise<void> {
261269
const groupedResults = groupBy(importResult.result, (r) =>
262270
existingProject.findSpecific(r.type, r.name)?.sourceMapKey?.split('#')?.[0] ?? 'unknown'
@@ -277,10 +285,17 @@ ${JSON.stringify(unsupportedTypeIds)}`);
277285
ImportOrchestrator.attachResourceInfo(existing.resourceConfigs, resourceInfoList);
278286

279287
const modificationCalculator = new FileModificationCalculator(existing);
280-
const modification = modificationCalculator.calculate(imported.map((resource) => ({
281-
modification: ModificationType.INSERT_OR_UPDATE,
282-
resource
283-
})));
288+
const modification = await modificationCalculator.calculate(
289+
imported.map((resource) => ({
290+
modification: ModificationType.INSERT_OR_UPDATE,
291+
resource
292+
})),
293+
// Handle matching here since we need the plugin to determine if two configs represent the same underlying resource
294+
async (resource, array) => {
295+
const match = await pluginManager.match(resource, array.filter((r) => r.type === resource.type));
296+
return array.findIndex((i) => i.isDeepEqual(match));
297+
}
298+
);
284299

285300
return { file: filePath!, modification };
286301
}));

src/plugins/plugin-manager.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { InternalError } from '../common/errors.js';
88
import { Plan, ResourcePlan } from '../entities/plan.js';
99
import { Project } from '../entities/project.js';
10+
import { ResourceConfig } from '../entities/resource-config.js';
1011
import { ResourceInfo } from '../entities/resource-info.js';
1112
import { SubProcessName, ctx } from '../events/context.js';
1213
import { groupBy } from '../utils/index.js';
@@ -73,6 +74,25 @@ export class PluginManager {
7374
return ResourceInfo.fromResponseData(result);
7475
}
7576

77+
async match(resource: ResourceConfig, array: ResourceConfig[]): Promise<ResourceConfig | null> {
78+
const pluginName = this.resourceToPluginMapping.get(resource.type);
79+
if (!pluginName) {
80+
throw new Error(`Unable to find plugin for resource: ${resource.type}`);
81+
}
82+
83+
const plugin = this.plugins.get(pluginName)
84+
if (!plugin) {
85+
throw new Error(`Unable to find plugin for resource ${resource.type}`);
86+
}
87+
88+
const { match } = await plugin.match(resource, array);
89+
if (!match) {
90+
return null;
91+
}
92+
93+
return ResourceConfig.fromJson(match);
94+
}
95+
7696

7797
async importResource(config: ResourceJson): Promise<ImportResponseData> {
7898
const pluginName = this.resourceToPluginMapping.get(config.core.type);

src/plugins/plugin.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
ImportResponseDataSchema,
66
InitializeResponseData,
77
InitializeResponseDataSchema,
8+
MatchResponseData,
9+
MatchResponseDataSchema,
810
PlanRequestData,
911
PlanResponseData,
1012
PlanResponseDataSchema,
@@ -21,6 +23,7 @@ import { PluginProcess } from './plugin-process.js';
2123
const initializeResponseValidator = ajv.compile(InitializeResponseDataSchema);
2224
const validateResponseValidator = ajv.compile(ValidateResponseDataSchema);
2325
const getResourceInfoResponseValidator = ajv.compile(GetResourceInfoResponseDataSchema);
26+
const matchResponseValidator = ajv.compile(MatchResponseDataSchema);
2427
const importResponseValidator = ajv.compile(ImportResponseDataSchema);
2528
const planResponseValidator = ajv.compile(PlanResponseDataSchema);
2629

@@ -92,6 +95,24 @@ export class Plugin implements IPlugin {
9295
return result.data;
9396
}
9497

98+
async match(resource: ResourceConfig, array: ResourceConfig[]): Promise<MatchResponseData> {
99+
const result = await this.process!.sendMessageForResult('match', {
100+
resource: resource.toJson(),
101+
array: array.map((r) => r.toJson()),
102+
});
103+
104+
if (!result.isSuccessful()) {
105+
throw new Error(`Unable to match resource: "${resource.type}" from plugin: "${this.name}" \n\n` + result.data);
106+
}
107+
108+
if (!this.validateMatchResponse(result.data)) {
109+
throw new Error(`Plugin error: Invalid get resource info response from plugin: ${this.name}`);
110+
}
111+
112+
return result.data;
113+
}
114+
115+
95116
async import(config: ResourceJson): Promise<ImportResponseData> {
96117
const result = await this.process!.sendMessageForResult('import', config);
97118

@@ -147,6 +168,10 @@ export class Plugin implements IPlugin {
147168
return getResourceInfoResponseValidator(response)
148169
}
149170

171+
private validateMatchResponse(response: unknown): response is MatchResponseData {
172+
return matchResponseValidator(response)
173+
}
174+
150175
private validateImportResponse(response: unknown): response is ImportResponseData {
151176
return importResponseValidator(response)
152177
}

0 commit comments

Comments
 (0)