Skip to content

Commit a18b107

Browse files
committed
feat: Added new isSameOnSystem method on resource entity + tests
1 parent e3b3a08 commit a18b107

File tree

4 files changed

+186
-4
lines changed

4 files changed

+186
-4
lines changed

src/entities/resource-config.test.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//import { ProjectConfig } from './project.js';
22
import { describe, expect, it } from 'vitest';
33
import { ResourceConfig } from './resource-config';
4+
import { ResourceInfo } from './resource-info';
45

5-
describe('Parser: project entity tests', () => {
6+
describe('Resource config unit tests', () => {
67
it('parses an empty project', () => {
78
expect(new ResourceConfig({
89
type: 'anything',
@@ -23,4 +24,104 @@ describe('Parser: project entity tests', () => {
2324

2425
it('plugin versions must be semvers', () => {
2526
})
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+
})
26127
});

src/entities/resource-config.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ResourceJson, ResourceConfig as SchemaResourceConfig } from 'codify-schemas';
22

3+
import { deepEqual } from '../utils/index.js';
34
import { ConfigBlock, ConfigType } from './config.js';
5+
import { ResourceInfo } from './resource-info.js';
46

57
/** Resource JSON supported format
68
* {
@@ -32,6 +34,8 @@ export class ResourceConfig implements ConfigBlock {
3234
dependencyIds: string[] = []; // id of other nodes
3335
parameters: Record<string, unknown>;
3436

37+
resourceInfo?: ResourceInfo;
38+
3539
constructor(config: SchemaResourceConfig, sourceMapKey?: string) {
3640
const { dependsOn, name, type, ...parameters } = config;
3741

@@ -69,6 +73,41 @@ export class ResourceConfig implements ConfigBlock {
6973
return externalId === this.id;
7074
}
7175

76+
/**
77+
* Useful for imports, creates and destroys. This checks if two resources represents the same installation on the system.
78+
*/
79+
isSameOnSystem(other: ResourceConfig, checkResourceInfo = true): boolean {
80+
if (other.type !== this.type) {
81+
return false;
82+
}
83+
84+
// If names are specified then that means Codify intends for the resources to be the same
85+
if (other.name && this.name && other.name !== this.name) {
86+
return false;
87+
}
88+
89+
if (!checkResourceInfo) {
90+
return true;
91+
}
92+
93+
if (!this.resourceInfo || !other.resourceInfo) {
94+
throw new Error(`checkResourceInfo specified but no resource info provided (${this.type}) (other: ${other.type})`)
95+
}
96+
97+
const thisRequiredKeys = new Set(this.resourceInfo.getRequiredParameters().map((p) => p.name));
98+
const otherRequiredKeys = new Set(other.resourceInfo.getRequiredParameters().map((p) => p.name));
99+
100+
const thisRequiredParameters = Object.fromEntries(Object.entries(this.parameters)
101+
.filter(([k]) => thisRequiredKeys.has(k))
102+
);
103+
const otherRequiredParameters = Object.fromEntries(Object.entries(other.parameters)
104+
.filter(([k]) => otherRequiredKeys.has(k))
105+
);
106+
107+
return deepEqual(thisRequiredParameters, otherRequiredParameters);
108+
109+
}
110+
72111
setName(name: string) {
73112
this.name = name;
74113
this.raw.name = name;
@@ -113,4 +152,12 @@ export class ResourceConfig implements ConfigBlock {
113152
addDependencies(dependencies: string[]) {
114153
this.dependencyIds.push(...dependencies);
115154
}
155+
156+
attachResourceInfo(resourceInfo: ResourceInfo) {
157+
if (resourceInfo.type !== this.type) {
158+
throw new Error(`Attempting to attach resource info (${resourceInfo.type}) on an un-related resource (${this.type})`)
159+
}
160+
161+
this.resourceInfo = resourceInfo;
162+
}
116163
}

src/entities/resource-info.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ interface ParameterInfo {
55
type?: string;
66
description?: string;
77
isRequired: boolean;
8-
value: unknown;
8+
value?: unknown;
99
}
1010

1111
export class ResourceInfo implements GetResourceInfoResponseData {
@@ -15,6 +15,8 @@ export class ResourceInfo implements GetResourceInfoResponseData {
1515
dependencies?: string[] | undefined;
1616
import?: { requiredParameters: null | string[]; } | undefined;
1717

18+
private constructor() {}
19+
1820
get description(): string | undefined {
1921
return this.schema?.description as string | undefined;
2022
}
@@ -31,14 +33,16 @@ export class ResourceInfo implements GetResourceInfoResponseData {
3133
return [];
3234
}
3335

34-
const { properties } = schema;
36+
const { properties, required } = schema;
3537
if (!properties || typeof properties !== 'object') {
3638
return [];
3739
}
3840

3941
return Object.entries(properties)
4042
.map(([propertyName, info]) => {
41-
const isRequired = this.import?.requiredParameters?.some((name) => name === propertyName) ?? false
43+
const isRequired = this.import?.requiredParameters?.some((name) => name === propertyName)
44+
?? (required as string[] | undefined)?.includes(propertyName)
45+
?? false;
4246

4347
return {
4448
name: propertyName,

src/utils/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,33 @@ export function sleep(ms: number): Promise<void> {
2929
setTimeout(resolve, ms)
3030
});
3131
}
32+
33+
export function deepEqual(obj1: unknown, obj2: unknown): boolean {
34+
// Base case: If both objects are identical, return true.
35+
if (obj1 === obj2) {
36+
return true;
37+
}
38+
39+
// Check if both objects are objects and not null.
40+
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) {
41+
return false;
42+
}
43+
44+
// Get the keys of both objects.
45+
const keys1 = Object.keys(obj1);
46+
const keys2 = Object.keys(obj2);
47+
// Check if the number of keys is the same.
48+
if (keys1.length !== keys2.length) {
49+
return false;
50+
}
51+
52+
// Iterate through the keys and compare their values recursively.
53+
for (const key of keys1) {
54+
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
55+
return false;
56+
}
57+
}
58+
59+
// If all checks pass, the objects are deep equal.
60+
return true;
61+
}

0 commit comments

Comments
 (0)