Skip to content

Commit b7f00bc

Browse files
[CODE-39] Default value support (#2)
* Added default value support for parameter configs * Add default values for apply * Add default values for apply as well when planId isn't passed in. The previousValue can be calculated based on the resource operation
1 parent 1605052 commit b7f00bc

6 files changed

Lines changed: 298 additions & 14 deletions

File tree

src/entities/plan.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Plan } from './plan.js';
3+
import { TestResource } from './resource.test.js';
4+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
5+
import { Resource } from './resource.js';
6+
7+
describe('Plan entity tests', () => {
8+
it('Adds default values properly when plan is parsed from request (Create)', () => {
9+
const resource = createResource();
10+
11+
const plan = Plan.fromResponse({
12+
operation: ResourceOperation.CREATE,
13+
resourceType: 'type',
14+
parameters: [{
15+
name: 'propB',
16+
operation: ParameterOperation.ADD,
17+
previousValue: null,
18+
newValue: 'propBValue'
19+
}]
20+
}, resource.defaultValues);
21+
22+
expect(plan.currentConfig).toMatchObject({
23+
type: 'type',
24+
propA: null,
25+
propB: null,
26+
})
27+
28+
expect(plan.desiredConfig).toMatchObject({
29+
type: 'type',
30+
propA: 'defaultA',
31+
propB: 'propBValue',
32+
})
33+
34+
expect(plan.changeSet.parameterChanges
35+
.every((pc) => pc.operation === ParameterOperation.ADD)
36+
).to.be.true;
37+
})
38+
39+
it('Adds default values properly when plan is parsed from request (Destroy)', () => {
40+
const resource = createResource();
41+
42+
const plan = Plan.fromResponse({
43+
operation: ResourceOperation.DESTROY,
44+
resourceType: 'type',
45+
parameters: [{
46+
name: 'propB',
47+
operation: ParameterOperation.REMOVE,
48+
previousValue: 'propBValue',
49+
newValue: null,
50+
}]
51+
}, resource.defaultValues);
52+
53+
expect(plan.currentConfig).toMatchObject({
54+
type: 'type',
55+
propA: 'defaultA',
56+
propB: 'propBValue',
57+
})
58+
59+
expect(plan.desiredConfig).toMatchObject({
60+
type: 'type',
61+
propA: null,
62+
propB: null,
63+
})
64+
65+
expect(plan.changeSet.parameterChanges
66+
.every((pc) => pc.operation === ParameterOperation.REMOVE)
67+
).to.be.true;
68+
})
69+
70+
it('Adds default values properly when plan is parsed from request (No-op)', () => {
71+
const resource = createResource();
72+
73+
const plan = Plan.fromResponse({
74+
operation: ResourceOperation.NOOP,
75+
resourceType: 'type',
76+
parameters: [{
77+
name: 'propB',
78+
operation: ParameterOperation.NOOP,
79+
previousValue: 'propBValue',
80+
newValue: 'propBValue',
81+
}]
82+
}, resource.defaultValues);
83+
84+
expect(plan.currentConfig).toMatchObject({
85+
type: 'type',
86+
propA: 'defaultA',
87+
propB: 'propBValue',
88+
})
89+
90+
expect(plan.desiredConfig).toMatchObject({
91+
type: 'type',
92+
propA: 'defaultA',
93+
propB: 'propBValue',
94+
})
95+
96+
expect(plan.changeSet.parameterChanges
97+
.every((pc) => pc.operation === ParameterOperation.NOOP)
98+
).to.be.true;
99+
})
100+
101+
it('Does not add default value if a value has already been specified', () => {
102+
const resource = createResource();
103+
104+
const plan = Plan.fromResponse({
105+
operation: ResourceOperation.CREATE,
106+
resourceType: 'type',
107+
parameters: [{
108+
name: 'propB',
109+
operation: ParameterOperation.ADD,
110+
previousValue: null,
111+
newValue: 'propBValue',
112+
}, {
113+
name: 'propA',
114+
operation: ParameterOperation.ADD,
115+
previousValue: null,
116+
newValue: 'propAValue',
117+
}]
118+
}, resource.defaultValues);
119+
120+
expect(plan.currentConfig).toMatchObject({
121+
type: 'type',
122+
propA: null,
123+
propB: null,
124+
})
125+
126+
expect(plan.desiredConfig).toMatchObject({
127+
type: 'type',
128+
propA: 'propAValue',
129+
propB: 'propBValue',
130+
})
131+
132+
expect(plan.changeSet.parameterChanges
133+
.every((pc) => pc.operation === ParameterOperation.ADD)
134+
).to.be.true;
135+
})
136+
})
137+
138+
function createResource(): Resource<any> {
139+
return new class extends TestResource {
140+
constructor() {
141+
super({
142+
type: 'type',
143+
parameterConfigurations: {
144+
propA: {
145+
defaultValue: 'defaultA'
146+
}
147+
}
148+
});
149+
}
150+
}
151+
}

src/entities/plan.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,29 +75,70 @@ export class Plan<T extends StringIndexedObject> {
7575
return this.resourceMetadata.type
7676
}
7777

78-
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T> {
78+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues: Partial<Record<keyof T, unknown>>): Plan<T> {
7979
if (!data) {
8080
throw new Error('Data is empty');
8181
}
8282

83+
addDefaultValues();
84+
8385
return new Plan(
8486
randomUUID(),
8587
new ChangeSet<T>(
8688
data.operation,
87-
data.parameters.map(value => ({
88-
...value,
89-
previousValue: null,
90-
})),
89+
data.parameters
9190
),
9291
{
9392
type: data.resourceType,
9493
name: data.resourceName,
95-
...(data.parameters.reduce(
96-
(prev, { name, newValue }) => Object.assign(prev, { [name]: newValue }),
97-
{}
98-
))
9994
},
10095
);
96+
97+
function addDefaultValues(): void {
98+
Object.entries(defaultValues)
99+
.forEach(([key, defaultValue]) => {
100+
const configValueExists = data
101+
?.parameters
102+
.find((p) => p.name === key) !== undefined;
103+
104+
if (!configValueExists) {
105+
switch (data?.operation) {
106+
case ResourceOperation.CREATE: {
107+
data?.parameters.push({
108+
name: key,
109+
operation: ParameterOperation.ADD,
110+
previousValue: null,
111+
newValue: defaultValue,
112+
});
113+
break;
114+
}
115+
116+
case ResourceOperation.DESTROY: {
117+
data?.parameters.push({
118+
name: key,
119+
operation: ParameterOperation.REMOVE,
120+
previousValue: defaultValue,
121+
newValue: null,
122+
});
123+
break;
124+
}
125+
126+
case ResourceOperation.MODIFY:
127+
case ResourceOperation.RECREATE:
128+
case ResourceOperation.NOOP: {
129+
data?.parameters.push({
130+
name: key,
131+
operation: ParameterOperation.NOOP,
132+
previousValue: defaultValue,
133+
newValue: defaultValue,
134+
});
135+
break;
136+
}
137+
}
138+
}
139+
});
140+
}
141+
101142
}
102143

103144
get desiredConfig(): T {

src/entities/plugin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ export class Plugin {
9494
return this.planStorage.get(planId)!
9595
}
9696

97-
return Plan.fromResponse(data.plan);
97+
if (!planRequest?.resourceName || !this.resources.has(planRequest.resourceName)) {
98+
throw new Error('Malformed plan. Resource name must be supplied');
99+
}
100+
101+
const resource = this.resources.get(planRequest.resourceName);
102+
return Plan.fromResponse(data.plan, resource?.defaultValues!);
98103
}
99104

100105
protected async crossValidateResources(configs: ResourceConfig[]): Promise<void> {}

src/entities/resource-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export interface ResourceParameterConfiguration {
1717
* @param current
1818
*/
1919
isEqual?: (desired: any, current: any) => boolean;
20+
/**
21+
* Default value for the parameter. If a value is not provided in the config, the library will use this value.
22+
*/
23+
defaultValue?: unknown,
2024
}
2125

2226
/**

src/entities/resource.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,61 @@ describe('Resource tests', () => {
293293
}).to.not.throw;
294294
})
295295

296+
it('Allows default values to be added', async () => {
297+
const resource = new class extends TestResource {
298+
constructor() {
299+
super({
300+
type: 'type',
301+
parameterConfigurations: {
302+
propA: { defaultValue: 'propADefault' }
303+
}
304+
});
305+
}
306+
307+
// @ts-ignore
308+
async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
309+
expect(desired.has('propA')).to.be.true;
310+
expect(desired.get('propA')).to.be.eq('propADefault');
311+
312+
return {
313+
propA: 'propAAfter'
314+
};
315+
}
316+
}
317+
318+
const plan = await resource.plan({ type: 'resource'})
319+
expect(plan.currentConfig.propA).to.eq('propAAfter');
320+
expect(plan.desiredConfig.propA).to.eq('propADefault');
321+
expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
322+
323+
})
324+
325+
it('Allows default values to be added (ignore default value if already present)', async () => {
326+
const resource = new class extends TestResource {
327+
constructor() {
328+
super({
329+
type: 'type',
330+
parameterConfigurations: {
331+
propA: { defaultValue: 'propADefault' }
332+
}
333+
});
334+
}
335+
336+
// @ts-ignore
337+
async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
338+
expect(desired.has('propA')).to.be.true;
339+
expect(desired.get('propA')).to.be.eq('propA');
340+
341+
return {
342+
propA: 'propAAfter'
343+
};
344+
}
345+
}
346+
347+
const plan = await resource.plan({ type: 'resource', propA: 'propA'})
348+
expect(plan.currentConfig.propA).to.eq('propAAfter');
349+
expect(plan.desiredConfig.propA).to.eq('propA');
350+
expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
351+
352+
})
296353
});

0 commit comments

Comments
 (0)