codify-plugin-lib 1.0.41 → 1.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entities/change-set.d.ts +1 -2
- package/dist/entities/change-set.js +16 -23
- package/dist/entities/plan-types.d.ts +1 -0
- package/dist/entities/plan.d.ts +1 -1
- package/dist/entities/plan.js +3 -6
- package/dist/entities/resource.js +3 -3
- package/dist/entities/stateful-parameter.d.ts +9 -4
- package/dist/entities/stateful-parameter.js +15 -2
- package/package.json +1 -1
- package/src/entities/change-set.ts +36 -26
- package/src/entities/plan-types.ts +2 -0
- package/src/entities/plan.ts +5 -8
- package/src/entities/resource-parameters.test.ts +2 -1
- package/src/entities/resource.test.ts +6 -3
- package/src/entities/resource.ts +5 -4
- package/src/entities/stateful-parameter.test.ts +118 -0
- package/src/entities/stateful-parameter.ts +28 -6
|
@@ -12,13 +12,12 @@ export declare class ChangeSet<T extends StringIndexedObject> {
|
|
|
12
12
|
constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
|
|
13
13
|
get desiredParameters(): T;
|
|
14
14
|
get currentParameters(): T;
|
|
15
|
-
static newCreate<T extends {}>(desiredConfig: T): ChangeSet<StringIndexedObject>;
|
|
16
15
|
static calculateParameterChangeSet<T extends StringIndexedObject>(desired: T | null, current: T | null, options: {
|
|
17
16
|
statefulMode: boolean;
|
|
18
17
|
parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
|
|
19
18
|
}): ParameterChange<T>[];
|
|
20
19
|
static combineResourceOperations(prev: ResourceOperation, next: ResourceOperation): ResourceOperation;
|
|
21
|
-
static isSame(
|
|
20
|
+
static isSame(desired: unknown, current: unknown, configuration?: ParameterConfiguration): boolean;
|
|
22
21
|
private static calculateStatefulModeChangeSet;
|
|
23
22
|
private static calculateStatelessModeChangeSet;
|
|
24
23
|
}
|
|
@@ -20,19 +20,6 @@ export class ChangeSet {
|
|
|
20
20
|
[pc.name]: pc.previousValue,
|
|
21
21
|
}), {});
|
|
22
22
|
}
|
|
23
|
-
static newCreate(desiredConfig) {
|
|
24
|
-
const parameterChangeSet = Object.entries(desiredConfig)
|
|
25
|
-
.filter(([k,]) => k !== 'type' && k !== 'name')
|
|
26
|
-
.map(([k, v]) => {
|
|
27
|
-
return {
|
|
28
|
-
name: k,
|
|
29
|
-
operation: ParameterOperation.ADD,
|
|
30
|
-
previousValue: null,
|
|
31
|
-
newValue: v,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
|
|
35
|
-
}
|
|
36
23
|
static calculateParameterChangeSet(desired, current, options) {
|
|
37
24
|
if (options.statefulMode) {
|
|
38
25
|
return ChangeSet.calculateStatefulModeChangeSet(desired, current, options.parameterConfigurations);
|
|
@@ -53,16 +40,22 @@ export class ChangeSet {
|
|
|
53
40
|
const indexNext = orderOfOperations.indexOf(next);
|
|
54
41
|
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
55
42
|
}
|
|
56
|
-
static isSame(
|
|
57
|
-
if (isEqual) {
|
|
58
|
-
return isEqual(
|
|
43
|
+
static isSame(desired, current, configuration) {
|
|
44
|
+
if (configuration?.isEqual) {
|
|
45
|
+
return configuration.isEqual(desired, current);
|
|
59
46
|
}
|
|
60
|
-
if (Array.isArray(
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
47
|
+
if (Array.isArray(desired) && Array.isArray(current)) {
|
|
48
|
+
const sortedDesired = desired.map((x) => x).sort();
|
|
49
|
+
const sortedCurrent = current.map((x) => x).sort();
|
|
50
|
+
if (sortedDesired.length !== sortedCurrent.length) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (configuration?.isElementEqual) {
|
|
54
|
+
return sortedDesired.every((value, index) => configuration.isElementEqual(value, sortedCurrent[index]));
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(sortedDesired) === JSON.stringify(sortedCurrent);
|
|
64
57
|
}
|
|
65
|
-
return
|
|
58
|
+
return desired === current;
|
|
66
59
|
}
|
|
67
60
|
static calculateStatefulModeChangeSet(desired, current, parameterConfigurations) {
|
|
68
61
|
const parameterChangeSet = new Array();
|
|
@@ -79,7 +72,7 @@ export class ChangeSet {
|
|
|
79
72
|
delete _current[k];
|
|
80
73
|
continue;
|
|
81
74
|
}
|
|
82
|
-
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]
|
|
75
|
+
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
|
|
83
76
|
parameterChangeSet.push({
|
|
84
77
|
name: k,
|
|
85
78
|
previousValue: v,
|
|
@@ -126,7 +119,7 @@ export class ChangeSet {
|
|
|
126
119
|
});
|
|
127
120
|
continue;
|
|
128
121
|
}
|
|
129
|
-
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]
|
|
122
|
+
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
|
|
130
123
|
parameterChangeSet.push({
|
|
131
124
|
name: k,
|
|
132
125
|
previousValue: _current[k],
|
|
@@ -2,6 +2,7 @@ import { ResourceOperation } from 'codify-schemas';
|
|
|
2
2
|
export interface ParameterConfiguration {
|
|
3
3
|
planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
|
|
4
4
|
isEqual?: (desired: any, current: any) => boolean;
|
|
5
|
+
isElementEqual?: (desired: any, current: any) => boolean;
|
|
5
6
|
isStatefulParameter?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export interface PlanConfiguration<T> {
|
package/dist/entities/plan.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
6
6
|
changeSet: ChangeSet<T>;
|
|
7
7
|
resourceMetadata: ResourceConfig;
|
|
8
8
|
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
|
|
9
|
-
static create<T extends StringIndexedObject>(
|
|
9
|
+
static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, configuration: PlanConfiguration<T>): Plan<T>;
|
|
10
10
|
getResourceType(): string;
|
|
11
11
|
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
|
|
12
12
|
get desiredConfig(): T;
|
package/dist/entities/plan.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ChangeSet } from './change-set.js';
|
|
2
2
|
import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
|
-
import { splitUserConfig } from '../utils/utils.js';
|
|
5
4
|
export class Plan {
|
|
6
5
|
id;
|
|
7
6
|
changeSet;
|
|
@@ -11,19 +10,17 @@ export class Plan {
|
|
|
11
10
|
this.changeSet = changeSet;
|
|
12
11
|
this.resourceMetadata = resourceMetadata;
|
|
13
12
|
}
|
|
14
|
-
static create(
|
|
13
|
+
static create(desiredParameters, currentParameters, resourceMetadata, configuration) {
|
|
15
14
|
const parameterConfigurations = configuration.parameterConfigurations ?? {};
|
|
16
15
|
const statefulParameterNames = new Set([...Object.entries(parameterConfigurations)]
|
|
17
16
|
.filter(([k, v]) => v.isStatefulParameter)
|
|
18
17
|
.map(([k, v]) => k));
|
|
19
|
-
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
20
|
-
const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
|
|
21
18
|
const parameterChangeSet = ChangeSet.calculateParameterChangeSet(desiredParameters, currentParameters, { statefulMode: configuration.statefulMode, parameterConfigurations });
|
|
22
19
|
let resourceOperation;
|
|
23
|
-
if (!
|
|
20
|
+
if (!currentParameters && desiredParameters) {
|
|
24
21
|
resourceOperation = ResourceOperation.CREATE;
|
|
25
22
|
}
|
|
26
|
-
else if (
|
|
23
|
+
else if (currentParameters && !desiredParameters) {
|
|
27
24
|
resourceOperation = ResourceOperation.DESTROY;
|
|
28
25
|
}
|
|
29
26
|
else {
|
|
@@ -33,18 +33,18 @@ export class Resource {
|
|
|
33
33
|
const keysToRefresh = new Set(Object.keys(resourceParameters));
|
|
34
34
|
const currentParameters = await this.refresh(keysToRefresh);
|
|
35
35
|
if (currentParameters == null) {
|
|
36
|
-
return Plan.create(
|
|
36
|
+
return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
|
|
37
37
|
}
|
|
38
38
|
this.validateRefreshResults(currentParameters, keysToRefresh);
|
|
39
39
|
for (const statefulParameter of statefulParameters) {
|
|
40
40
|
const desiredValue = desiredParameters[statefulParameter.name];
|
|
41
|
-
let currentValue = await statefulParameter.refresh(
|
|
41
|
+
let currentValue = await statefulParameter.refresh() ?? undefined;
|
|
42
42
|
if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
|
|
43
43
|
currentValue = currentValue.filter((p) => desiredValue?.includes(p));
|
|
44
44
|
}
|
|
45
45
|
currentParameters[statefulParameter.name] = currentValue;
|
|
46
46
|
}
|
|
47
|
-
return Plan.create(
|
|
47
|
+
return Plan.create(desiredParameters, currentParameters, resourceMetadata, planConfiguration);
|
|
48
48
|
}
|
|
49
49
|
async apply(plan) {
|
|
50
50
|
if (plan.getResourceType() !== this.typeId) {
|
|
@@ -2,23 +2,28 @@ import { Plan } from './plan.js';
|
|
|
2
2
|
import { StringIndexedObject } from 'codify-schemas';
|
|
3
3
|
export interface StatefulParameterConfiguration<T> {
|
|
4
4
|
name: keyof T;
|
|
5
|
-
isEqual?: (
|
|
5
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {
|
|
8
|
+
isEqual?: (desired: any[], current: any[]) => boolean;
|
|
9
|
+
isElementEqual?: (desired: any, current: any) => boolean;
|
|
6
10
|
}
|
|
7
11
|
export declare abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
8
12
|
readonly name: keyof T;
|
|
9
13
|
readonly configuration: StatefulParameterConfiguration<T>;
|
|
10
14
|
protected constructor(configuration: StatefulParameterConfiguration<T>);
|
|
11
|
-
abstract refresh(
|
|
15
|
+
abstract refresh(): Promise<V | null>;
|
|
12
16
|
abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
13
17
|
abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
14
18
|
abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
|
|
15
19
|
}
|
|
16
20
|
export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
|
|
17
|
-
|
|
21
|
+
configuration: ArrayStatefulParameterConfiguration<T>;
|
|
22
|
+
constructor(configuration: ArrayStatefulParameterConfiguration<T>);
|
|
18
23
|
applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
|
|
19
24
|
applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
20
25
|
applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
|
|
21
|
-
abstract refresh(
|
|
26
|
+
abstract refresh(): Promise<V[] | null>;
|
|
22
27
|
abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
|
|
23
28
|
abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
|
|
24
29
|
}
|
|
@@ -7,8 +7,10 @@ export class StatefulParameter {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
export class ArrayStatefulParameter extends StatefulParameter {
|
|
10
|
+
configuration;
|
|
10
11
|
constructor(configuration) {
|
|
11
12
|
super(configuration);
|
|
13
|
+
this.configuration = configuration;
|
|
12
14
|
}
|
|
13
15
|
async applyAdd(valuesToAdd, plan) {
|
|
14
16
|
for (const value of valuesToAdd) {
|
|
@@ -16,8 +18,19 @@ export class ArrayStatefulParameter extends StatefulParameter {
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
async applyModify(newValues, previousValues, allowDeletes, plan) {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
+
const configuration = this.configuration;
|
|
22
|
+
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
23
|
+
if ((configuration).isElementEqual) {
|
|
24
|
+
return configuration.isElementEqual(n, p);
|
|
25
|
+
}
|
|
26
|
+
return n === p;
|
|
27
|
+
}));
|
|
28
|
+
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
29
|
+
if ((configuration).isElementEqual) {
|
|
30
|
+
return configuration.isElementEqual(n, p);
|
|
31
|
+
}
|
|
32
|
+
return n === p;
|
|
33
|
+
}));
|
|
21
34
|
for (const value of valuesToAdd) {
|
|
22
35
|
await this.applyAddItem(value, plan);
|
|
23
36
|
}
|
package/package.json
CHANGED
|
@@ -43,20 +43,20 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
43
43
|
// const operation = ChangeSet.combineResourceOperations(prev, );
|
|
44
44
|
// }
|
|
45
45
|
|
|
46
|
-
static newCreate<T extends {}>(desiredConfig: T) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
46
|
+
// static newCreate<T extends {}>(desiredConfig: T) {
|
|
47
|
+
// const parameterChangeSet = Object.entries(desiredConfig)
|
|
48
|
+
// .filter(([k,]) => k !== 'type' && k !== 'name')
|
|
49
|
+
// .map(([k, v]) => {
|
|
50
|
+
// return {
|
|
51
|
+
// name: k,
|
|
52
|
+
// operation: ParameterOperation.ADD,
|
|
53
|
+
// previousValue: null,
|
|
54
|
+
// newValue: v,
|
|
55
|
+
// }
|
|
56
|
+
// })
|
|
57
|
+
//
|
|
58
|
+
// return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
|
|
59
|
+
// }
|
|
60
60
|
|
|
61
61
|
static calculateParameterChangeSet<T extends StringIndexedObject>(
|
|
62
62
|
desired: T | null,
|
|
@@ -86,22 +86,32 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
static isSame(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
desired: unknown,
|
|
90
|
+
current: unknown,
|
|
91
|
+
configuration?: ParameterConfiguration,
|
|
92
92
|
): boolean {
|
|
93
|
-
if (isEqual) {
|
|
94
|
-
return isEqual(
|
|
93
|
+
if (configuration?.isEqual) {
|
|
94
|
+
return configuration.isEqual(desired, current);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
if (Array.isArray(
|
|
98
|
-
const
|
|
99
|
-
const
|
|
97
|
+
if (Array.isArray(desired) && Array.isArray(current)) {
|
|
98
|
+
const sortedDesired = desired.map((x) => x).sort();
|
|
99
|
+
const sortedCurrent = current.map((x) => x).sort();
|
|
100
|
+
|
|
101
|
+
if (sortedDesired.length !== sortedCurrent.length) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (configuration?.isElementEqual) {
|
|
106
|
+
return sortedDesired.every((value, index) =>
|
|
107
|
+
configuration.isElementEqual!(value, sortedCurrent[index])
|
|
108
|
+
);
|
|
109
|
+
}
|
|
100
110
|
|
|
101
|
-
return JSON.stringify(
|
|
111
|
+
return JSON.stringify(sortedDesired) === JSON.stringify(sortedCurrent);
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
return
|
|
114
|
+
return desired === current;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
// Explanation: Stateful mode means that codify maintains a stateful to keep track of resources it has added.
|
|
@@ -130,7 +140,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
130
140
|
continue;
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]
|
|
143
|
+
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
|
|
134
144
|
parameterChangeSet.push({
|
|
135
145
|
name: k,
|
|
136
146
|
previousValue: v,
|
|
@@ -194,7 +204,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
194
204
|
continue;
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]
|
|
207
|
+
if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
|
|
198
208
|
parameterChangeSet.push({
|
|
199
209
|
name: k,
|
|
200
210
|
previousValue: _current[k],
|
package/src/entities/plan.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from 'codify-schemas';
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
11
11
|
import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
|
|
12
|
-
import { splitUserConfig } from '../utils/utils.js';
|
|
13
12
|
|
|
14
13
|
export class Plan<T extends StringIndexedObject> {
|
|
15
14
|
id: string;
|
|
@@ -23,8 +22,9 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
static create<T extends StringIndexedObject>(
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
desiredParameters: Partial<T> | null,
|
|
26
|
+
currentParameters: Partial<T> | null,
|
|
27
|
+
resourceMetadata: ResourceConfig,
|
|
28
28
|
configuration: PlanConfiguration<T>
|
|
29
29
|
): Plan<T> {
|
|
30
30
|
const parameterConfigurations = configuration.parameterConfigurations ?? {} as Record<keyof T, ParameterConfiguration>;
|
|
@@ -34,9 +34,6 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
34
34
|
.map(([k, v]) => k)
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
-
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
38
|
-
const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
|
|
39
|
-
|
|
40
37
|
|
|
41
38
|
// TODO: After adding in state files, need to calculate deletes here
|
|
42
39
|
// Where current config exists and state config exists but desired config doesn't
|
|
@@ -51,9 +48,9 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
51
48
|
);
|
|
52
49
|
|
|
53
50
|
let resourceOperation: ResourceOperation;
|
|
54
|
-
if (!
|
|
51
|
+
if (!currentParameters && desiredParameters) {
|
|
55
52
|
resourceOperation = ResourceOperation.CREATE;
|
|
56
|
-
} else if (
|
|
53
|
+
} else if (currentParameters && !desiredParameters) {
|
|
57
54
|
resourceOperation = ResourceOperation.DESTROY;
|
|
58
55
|
} else {
|
|
59
56
|
resourceOperation = parameterChangeSet
|
|
@@ -50,8 +50,9 @@ describe('Resource parameters tests', () => {
|
|
|
50
50
|
const resourceSpy = spy(resource);
|
|
51
51
|
const result = await resourceSpy.apply(
|
|
52
52
|
Plan.create<TestConfig>(
|
|
53
|
-
{
|
|
53
|
+
{ propA: 'a', propB: 0, propC: 'b' },
|
|
54
54
|
null,
|
|
55
|
+
{ type: 'resource' },
|
|
55
56
|
{ statefulMode: false },
|
|
56
57
|
)
|
|
57
58
|
);
|
|
@@ -136,6 +136,7 @@ describe('Resource tests', () => {
|
|
|
136
136
|
const result = await resourceSpy.apply(
|
|
137
137
|
Plan.create<TestConfig>(
|
|
138
138
|
{ type: 'resource', propA: 'a', propB: 0 },
|
|
139
|
+
null,
|
|
139
140
|
{ type: 'resource' },
|
|
140
141
|
{ statefulMode: false },
|
|
141
142
|
)
|
|
@@ -154,8 +155,9 @@ describe('Resource tests', () => {
|
|
|
154
155
|
const resourceSpy = spy(resource);
|
|
155
156
|
const result = await resourceSpy.apply(
|
|
156
157
|
Plan.create<TestConfig>(
|
|
158
|
+
null,
|
|
159
|
+
{ propA: 'a', propB: 0 },
|
|
157
160
|
{ type: 'resource' },
|
|
158
|
-
{ type: 'resource', propA: 'a', propB: 0 },
|
|
159
161
|
{ statefulMode: true },
|
|
160
162
|
)
|
|
161
163
|
)
|
|
@@ -173,8 +175,9 @@ describe('Resource tests', () => {
|
|
|
173
175
|
const resourceSpy = spy(resource);
|
|
174
176
|
const result = await resourceSpy.apply(
|
|
175
177
|
Plan.create<TestConfig>(
|
|
176
|
-
{
|
|
177
|
-
{
|
|
178
|
+
{ propA: 'a', propB: 0 },
|
|
179
|
+
{ propA: 'b', propB: -1 },
|
|
180
|
+
{ type: 'resource' },
|
|
178
181
|
{ statefulMode: true },
|
|
179
182
|
)
|
|
180
183
|
);
|
package/src/entities/resource.ts
CHANGED
|
@@ -66,7 +66,7 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
66
66
|
|
|
67
67
|
// Short circuit here. If resource is non-existent, then there's no point checking stateful parameters
|
|
68
68
|
if (currentParameters == null) {
|
|
69
|
-
return Plan.create(
|
|
69
|
+
return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
this.validateRefreshResults(currentParameters, keysToRefresh);
|
|
@@ -76,7 +76,7 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
76
76
|
for(const statefulParameter of statefulParameters) {
|
|
77
77
|
const desiredValue = desiredParameters[statefulParameter.name];
|
|
78
78
|
|
|
79
|
-
let currentValue = await statefulParameter.refresh(
|
|
79
|
+
let currentValue = await statefulParameter.refresh() ?? undefined;
|
|
80
80
|
|
|
81
81
|
// In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
|
|
82
82
|
if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
|
|
@@ -87,8 +87,9 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return Plan.create(
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
desiredParameters,
|
|
91
|
+
currentParameters as Partial<T>,
|
|
92
|
+
resourceMetadata,
|
|
92
93
|
planConfiguration,
|
|
93
94
|
)
|
|
94
95
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ArrayStatefulParameter, ArrayStatefulParameterConfiguration, } from './stateful-parameter.js';
|
|
3
|
+
import { Plan } from './plan.js';
|
|
4
|
+
import { spy } from 'sinon';
|
|
5
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
6
|
+
|
|
7
|
+
interface TestConfig {
|
|
8
|
+
propA: string[];
|
|
9
|
+
[x: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
|
|
13
|
+
constructor(configuration?: ArrayStatefulParameterConfiguration<TestConfig>) {
|
|
14
|
+
super(configuration ?? {
|
|
15
|
+
name: 'propA'
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
|
|
20
|
+
async applyRemoveItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
|
|
21
|
+
|
|
22
|
+
async refresh(): Promise<string[] | null> {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
describe('Stateful parameter tests', () => {
|
|
29
|
+
it('applyAddItem is called the correct number of times', async () => {
|
|
30
|
+
const plan = Plan.create<TestConfig>(
|
|
31
|
+
{ propA: ['a', 'b', 'c'] },
|
|
32
|
+
null,
|
|
33
|
+
{ type: 'typeA' },
|
|
34
|
+
{ statefulMode: false }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
38
|
+
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
39
|
+
|
|
40
|
+
const testParameter = spy(new TestArrayParameter());
|
|
41
|
+
await testParameter.applyAdd(plan.desiredConfig.propA, plan);
|
|
42
|
+
|
|
43
|
+
expect(testParameter.applyAddItem.callCount).to.eq(3);
|
|
44
|
+
expect(testParameter.applyRemoveItem.called).to.be.false;
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('applyRemoveItem is called the correct number of times', async () => {
|
|
48
|
+
const plan = Plan.create<TestConfig>(
|
|
49
|
+
null,
|
|
50
|
+
{ propA: ['a', 'b', 'c'] },
|
|
51
|
+
{ type: 'typeA' },
|
|
52
|
+
{ statefulMode: true }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
|
|
56
|
+
expect(plan.changeSet.parameterChanges.length).to.eq(1);
|
|
57
|
+
|
|
58
|
+
const testParameter = spy(new TestArrayParameter());
|
|
59
|
+
await testParameter.applyRemove(plan.currentConfig.propA, plan);
|
|
60
|
+
|
|
61
|
+
expect(testParameter.applyAddItem.called).to.be.false;
|
|
62
|
+
expect(testParameter.applyRemoveItem.callCount).to.eq(3);
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('In stateless mode only applyAddItem is called only for modifies', async () => {
|
|
66
|
+
const plan = Plan.create<TestConfig>(
|
|
67
|
+
{ propA: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
|
|
68
|
+
{ propA: ['a', 'b', 'c'] },
|
|
69
|
+
{ type: 'typeA' },
|
|
70
|
+
{ statefulMode: true, parameterConfigurations: { propA: { isStatefulParameter: true }} }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
74
|
+
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
75
|
+
name: 'propA',
|
|
76
|
+
previousValue: ['a', 'b', 'c'],
|
|
77
|
+
newValue: ['a', 'c', 'd', 'e', 'f'],
|
|
78
|
+
operation: ParameterOperation.MODIFY,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const testParameter = spy(new TestArrayParameter());
|
|
82
|
+
await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
|
|
83
|
+
|
|
84
|
+
expect(testParameter.applyAddItem.calledThrice).to.be.true;
|
|
85
|
+
expect(testParameter.applyRemoveItem.called).to.be.false;
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('isElementEqual is called for modifies', async () => {
|
|
89
|
+
const plan = Plan.create<TestConfig>(
|
|
90
|
+
{ propA: ['9.12', '9.13'] }, // b to remove, d, e, f to add
|
|
91
|
+
{ propA: ['9.12.9'] },
|
|
92
|
+
{ type: 'typeA' },
|
|
93
|
+
{ statefulMode: false, parameterConfigurations: { propA: { isStatefulParameter: true }} }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
97
|
+
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
98
|
+
name: 'propA',
|
|
99
|
+
previousValue: ['9.12.9'],
|
|
100
|
+
newValue: ['9.12', '9.13'],
|
|
101
|
+
operation: ParameterOperation.MODIFY,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const testParameter = spy(new class extends TestArrayParameter {
|
|
105
|
+
constructor() {
|
|
106
|
+
super({
|
|
107
|
+
name: 'propA',
|
|
108
|
+
isElementEqual: (desired, current) => current.includes(desired),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
|
|
114
|
+
|
|
115
|
+
expect(testParameter.applyAddItem.calledOnce).to.be.true;
|
|
116
|
+
expect(testParameter.applyRemoveItem.called).to.be.false;
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -3,9 +3,15 @@ import { StringIndexedObject } from 'codify-schemas';
|
|
|
3
3
|
|
|
4
4
|
export interface StatefulParameterConfiguration<T> {
|
|
5
5
|
name: keyof T;
|
|
6
|
-
isEqual?: (
|
|
6
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {
|
|
10
|
+
isEqual?: (desired: any[], current: any[]) => boolean;
|
|
11
|
+
isElementEqual?: (desired: any, current: any) => boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
10
16
|
readonly name: keyof T;
|
|
11
17
|
readonly configuration: StatefulParameterConfiguration<T>;
|
|
@@ -15,7 +21,7 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
|
|
|
15
21
|
this.configuration = configuration
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
abstract refresh(
|
|
24
|
+
abstract refresh(): Promise<V | null>;
|
|
19
25
|
|
|
20
26
|
// TODO: Add an additional parameter here for what has actually changed.
|
|
21
27
|
abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
@@ -24,8 +30,11 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
|
|
27
|
-
|
|
33
|
+
configuration: ArrayStatefulParameterConfiguration<T>;
|
|
34
|
+
|
|
35
|
+
constructor(configuration: ArrayStatefulParameterConfiguration<T>) {
|
|
28
36
|
super(configuration);
|
|
37
|
+
this.configuration = configuration;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
|
|
@@ -35,8 +44,21 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
|
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
|
|
38
|
-
const
|
|
39
|
-
|
|
47
|
+
const configuration = this.configuration as ArrayStatefulParameterConfiguration<T>;
|
|
48
|
+
|
|
49
|
+
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
50
|
+
if ((configuration).isElementEqual) {
|
|
51
|
+
return configuration.isElementEqual(n, p);
|
|
52
|
+
}
|
|
53
|
+
return n === p;
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
57
|
+
if ((configuration).isElementEqual) {
|
|
58
|
+
return configuration.isElementEqual(n, p);
|
|
59
|
+
}
|
|
60
|
+
return n === p;
|
|
61
|
+
}));
|
|
40
62
|
|
|
41
63
|
for (const value of valuesToAdd) {
|
|
42
64
|
await this.applyAddItem(value, plan)
|
|
@@ -55,7 +77,7 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
|
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
abstract refresh(
|
|
80
|
+
abstract refresh(): Promise<V[] | null>;
|
|
59
81
|
abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
|
|
60
82
|
abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
|
|
61
83
|
}
|