codify-plugin-lib 1.0.37 → 1.0.39
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 +17 -9
- package/dist/entities/change-set.js +91 -36
- package/dist/entities/plan-types.d.ts +11 -0
- package/dist/entities/plan-types.js +1 -0
- package/dist/entities/plan.d.ts +10 -7
- package/dist/entities/plan.js +53 -8
- package/dist/entities/plugin.js +9 -8
- package/dist/entities/resource-types.d.ts +24 -0
- package/dist/entities/resource-types.js +1 -0
- package/dist/entities/resource.d.ts +20 -15
- package/dist/entities/resource.js +137 -62
- package/dist/entities/stateful-parameter.d.ts +22 -8
- package/dist/entities/stateful-parameter.js +33 -0
- package/dist/entities/test.d.ts +1 -0
- package/dist/entities/test.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/utils/common-types.d.ts +3 -0
- package/dist/utils/common-types.js +1 -0
- package/dist/utils/utils.d.ts +6 -0
- package/dist/utils/utils.js +15 -0
- package/package.json +2 -2
- package/src/entities/change-set.test.ts +24 -36
- package/src/entities/change-set.ts +138 -47
- package/src/entities/plan-types.ts +26 -0
- package/src/entities/plan.ts +87 -16
- package/src/entities/plugin.ts +11 -9
- package/src/entities/resource-parameters.test.ts +159 -0
- package/src/entities/resource-types.ts +47 -0
- package/src/entities/resource.test.ts +77 -215
- package/src/entities/resource.ts +203 -91
- package/src/entities/stateful-parameter.ts +56 -8
- package/src/index.ts +3 -0
- package/src/utils/utils.ts +21 -0
|
@@ -1,25 +1,49 @@
|
|
|
1
|
-
import { ParameterOperation,
|
|
1
|
+
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { ParameterConfiguration } from './plan-types.js';
|
|
2
3
|
|
|
3
|
-
export interface ParameterChange {
|
|
4
|
-
name: string;
|
|
4
|
+
export interface ParameterChange<T extends StringIndexedObject> {
|
|
5
|
+
name: keyof T & string;
|
|
5
6
|
operation: ParameterOperation;
|
|
6
7
|
previousValue: any | null;
|
|
7
8
|
newValue: any | null;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export class ChangeSet {
|
|
11
|
+
export class ChangeSet<T extends StringIndexedObject> {
|
|
11
12
|
operation: ResourceOperation
|
|
12
|
-
parameterChanges: Array<ParameterChange
|
|
13
|
+
parameterChanges: Array<ParameterChange<T>>
|
|
13
14
|
|
|
14
15
|
constructor(
|
|
15
16
|
operation: ResourceOperation,
|
|
16
|
-
parameterChanges: Array<ParameterChange
|
|
17
|
+
parameterChanges: Array<ParameterChange<T>>
|
|
17
18
|
) {
|
|
18
19
|
this.operation = operation;
|
|
19
20
|
this.parameterChanges = parameterChanges;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
get desiredParameters(): T {
|
|
24
|
+
return this.parameterChanges
|
|
25
|
+
.reduce((obj, pc) => ({
|
|
26
|
+
...obj,
|
|
27
|
+
[pc.name]: pc.newValue,
|
|
28
|
+
}), {}) as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get currentParameters(): T {
|
|
32
|
+
return this.parameterChanges
|
|
33
|
+
.reduce((obj, pc) => ({
|
|
34
|
+
...obj,
|
|
35
|
+
[pc.name]: pc.previousValue,
|
|
36
|
+
}), {}) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// static create<T extends Record<string, unknown>>(prev: T, next: T, options: {
|
|
40
|
+
// statefulMode: boolean,
|
|
41
|
+
// }): ChangeSet {
|
|
42
|
+
// const parameterChanges = ChangeSet.calculateParameterChangeSet(prev, prev, options);
|
|
43
|
+
// const operation = ChangeSet.combineResourceOperations(prev, );
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
static newCreate<T extends {}>(desiredConfig: T) {
|
|
23
47
|
const parameterChangeSet = Object.entries(desiredConfig)
|
|
24
48
|
.filter(([k,]) => k !== 'type' && k !== 'name')
|
|
25
49
|
.map(([k, v]) => {
|
|
@@ -34,21 +58,67 @@ export class ChangeSet {
|
|
|
34
58
|
return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
static calculateParameterChangeSet
|
|
38
|
-
|
|
61
|
+
static calculateParameterChangeSet<T extends StringIndexedObject>(
|
|
62
|
+
desired: T | null,
|
|
63
|
+
current: T | null,
|
|
64
|
+
options: { statefulMode: boolean, parameterConfigurations?: Record<keyof T, ParameterConfiguration> },
|
|
65
|
+
): ParameterChange<T>[] {
|
|
66
|
+
if (options.statefulMode) {
|
|
67
|
+
return ChangeSet.calculateStatefulModeChangeSet(desired, current, options.parameterConfigurations);
|
|
68
|
+
} else {
|
|
69
|
+
return ChangeSet.calculateStatelessModeChangeSet(desired, current, options.parameterConfigurations);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
static combineResourceOperations(prev: ResourceOperation, next: ResourceOperation) {
|
|
74
|
+
const orderOfOperations = [
|
|
75
|
+
ResourceOperation.NOOP,
|
|
76
|
+
ResourceOperation.MODIFY,
|
|
77
|
+
ResourceOperation.RECREATE,
|
|
78
|
+
ResourceOperation.CREATE,
|
|
79
|
+
ResourceOperation.DESTROY,
|
|
80
|
+
]
|
|
44
81
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
.filter(([k,]) => k !== 'type' && k !== 'name')
|
|
48
|
-
);
|
|
82
|
+
const indexPrev = orderOfOperations.indexOf(prev);
|
|
83
|
+
const indexNext = orderOfOperations.indexOf(next);
|
|
49
84
|
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static isSame(
|
|
89
|
+
a: unknown,
|
|
90
|
+
b: unknown,
|
|
91
|
+
isEqual?: (a: unknown, b: unknown) => boolean,
|
|
92
|
+
): boolean {
|
|
93
|
+
if (isEqual) {
|
|
94
|
+
return isEqual(a, b);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
98
|
+
const sortedPrev = a.map((x) => x).sort();
|
|
99
|
+
const sortedNext = b.map((x) => x).sort();
|
|
100
|
+
|
|
101
|
+
return JSON.stringify(sortedPrev) === JSON.stringify(sortedNext);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return a === b;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Explanation: Stateful mode means that codify maintains a stateful to keep track of resources it has added.
|
|
108
|
+
// When a resource is removed from a stateful config, it will be deleted from the system.
|
|
109
|
+
private static calculateStatefulModeChangeSet<T extends StringIndexedObject>(
|
|
110
|
+
desired: T | null,
|
|
111
|
+
current: T | null,
|
|
112
|
+
parameterConfigurations?: Record<keyof T, ParameterConfiguration>,
|
|
113
|
+
): ParameterChange<T>[] {
|
|
114
|
+
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
115
|
+
|
|
116
|
+
const _desired = { ...desired };
|
|
117
|
+
const _current = { ...current };
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
for (const [k, v] of Object.entries(_current)) {
|
|
121
|
+
if (_desired[k] == null) {
|
|
52
122
|
parameterChangeSet.push({
|
|
53
123
|
name: k,
|
|
54
124
|
previousValue: v,
|
|
@@ -56,39 +126,39 @@ export class ChangeSet {
|
|
|
56
126
|
operation: ParameterOperation.REMOVE,
|
|
57
127
|
})
|
|
58
128
|
|
|
59
|
-
delete
|
|
129
|
+
delete _current[k];
|
|
60
130
|
continue;
|
|
61
131
|
}
|
|
62
132
|
|
|
63
|
-
if (!ChangeSet.isSame(
|
|
133
|
+
if (!ChangeSet.isSame(_current[k], _desired[k], parameterConfigurations?.[k]?.isEqual)) {
|
|
64
134
|
parameterChangeSet.push({
|
|
65
135
|
name: k,
|
|
66
136
|
previousValue: v,
|
|
67
|
-
newValue:
|
|
137
|
+
newValue: _desired[k],
|
|
68
138
|
operation: ParameterOperation.MODIFY,
|
|
69
139
|
})
|
|
70
140
|
|
|
71
|
-
delete
|
|
72
|
-
delete
|
|
141
|
+
delete _current[k];
|
|
142
|
+
delete _desired[k];
|
|
73
143
|
continue;
|
|
74
144
|
}
|
|
75
145
|
|
|
76
146
|
parameterChangeSet.push({
|
|
77
147
|
name: k,
|
|
78
148
|
previousValue: v,
|
|
79
|
-
newValue:
|
|
149
|
+
newValue: _desired[k],
|
|
80
150
|
operation: ParameterOperation.NOOP,
|
|
81
151
|
})
|
|
82
152
|
|
|
83
|
-
delete
|
|
84
|
-
delete
|
|
153
|
+
delete _current[k];
|
|
154
|
+
delete _desired[k];
|
|
85
155
|
}
|
|
86
156
|
|
|
87
|
-
if (Object.keys(
|
|
157
|
+
if (Object.keys(_current).length !== 0) {
|
|
88
158
|
throw Error('Diff algorithm error');
|
|
89
159
|
}
|
|
90
160
|
|
|
91
|
-
for (const [k, v] of Object.entries(
|
|
161
|
+
for (const [k, v] of Object.entries(_desired)) {
|
|
92
162
|
parameterChangeSet.push({
|
|
93
163
|
name: k,
|
|
94
164
|
previousValue: null,
|
|
@@ -100,29 +170,50 @@ export class ChangeSet {
|
|
|
100
170
|
return parameterChangeSet;
|
|
101
171
|
}
|
|
102
172
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
// Explanation: Stateful mode means that codify does not keep track of state. Resources in stateless mode can only
|
|
174
|
+
// be added by Codify and not destroyed.
|
|
175
|
+
private static calculateStatelessModeChangeSet<T extends StringIndexedObject>(
|
|
176
|
+
desired: T | null,
|
|
177
|
+
current: T | null,
|
|
178
|
+
parameterConfigurations?: Record<keyof T, ParameterConfiguration>,
|
|
179
|
+
): ParameterChange<T>[] {
|
|
180
|
+
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
111
181
|
|
|
112
|
-
const
|
|
113
|
-
const
|
|
182
|
+
const _desired = { ...desired };
|
|
183
|
+
const _current = { ...current };
|
|
114
184
|
|
|
115
|
-
|
|
116
|
-
|
|
185
|
+
for (const [k, v] of Object.entries(_desired)) {
|
|
186
|
+
if (_current[k] == null) {
|
|
187
|
+
parameterChangeSet.push({
|
|
188
|
+
name: k,
|
|
189
|
+
previousValue: null,
|
|
190
|
+
newValue: v,
|
|
191
|
+
operation: ParameterOperation.ADD,
|
|
192
|
+
});
|
|
117
193
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const sortedPrev = a.map((x) => x).sort();
|
|
121
|
-
const sortedNext = b.map((x) => x).sort();
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
122
196
|
|
|
123
|
-
|
|
197
|
+
if (!ChangeSet.isSame(_current[k], _desired[k], parameterConfigurations?.[k]?.isEqual)) {
|
|
198
|
+
parameterChangeSet.push({
|
|
199
|
+
name: k,
|
|
200
|
+
previousValue: _current[k],
|
|
201
|
+
newValue: _desired[k],
|
|
202
|
+
operation: ParameterOperation.MODIFY,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
parameterChangeSet.push({
|
|
209
|
+
name: k,
|
|
210
|
+
previousValue: v,
|
|
211
|
+
newValue: v,
|
|
212
|
+
operation: ParameterOperation.NOOP,
|
|
213
|
+
})
|
|
124
214
|
}
|
|
125
215
|
|
|
126
|
-
return
|
|
216
|
+
return parameterChangeSet;
|
|
127
217
|
}
|
|
218
|
+
|
|
128
219
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ResourceOperation } from 'codify-schemas';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Customize properties for specific parameters. This will alter the way the library process changes to the parameter.
|
|
5
|
+
*/
|
|
6
|
+
export interface ParameterConfiguration {
|
|
7
|
+
/**
|
|
8
|
+
* Chose if the resource should be re-created or modified if this parameter is changed. Defaults to re-create.
|
|
9
|
+
*/
|
|
10
|
+
planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
|
|
11
|
+
/**
|
|
12
|
+
* Customize the equality comparison for a parameter.
|
|
13
|
+
* @param a
|
|
14
|
+
* @param b
|
|
15
|
+
*/
|
|
16
|
+
isEqual?: (a: any, b: any) => boolean;
|
|
17
|
+
|
|
18
|
+
isArrayElementEqual?: (a: any, b: any) => boolean;
|
|
19
|
+
|
|
20
|
+
isStatefulParameter?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PlanConfiguration<T> {
|
|
24
|
+
statefulMode: boolean;
|
|
25
|
+
parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
|
|
26
|
+
}
|
package/src/entities/plan.ts
CHANGED
|
@@ -1,38 +1,95 @@
|
|
|
1
|
-
import { ChangeSet } from './change-set.js';
|
|
2
|
-
import {
|
|
1
|
+
import { ChangeSet, ParameterChange } from './change-set.js';
|
|
2
|
+
import {
|
|
3
|
+
ApplyRequestData,
|
|
4
|
+
ParameterOperation,
|
|
5
|
+
PlanResponseData,
|
|
6
|
+
ResourceConfig,
|
|
7
|
+
ResourceOperation,
|
|
8
|
+
StringIndexedObject,
|
|
9
|
+
} from 'codify-schemas';
|
|
3
10
|
import { randomUUID } from 'crypto';
|
|
11
|
+
import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
|
|
12
|
+
import { splitUserConfig } from '../utils/utils.js';
|
|
4
13
|
|
|
5
|
-
export class Plan<T extends
|
|
14
|
+
export class Plan<T extends StringIndexedObject> {
|
|
6
15
|
id: string;
|
|
7
|
-
changeSet: ChangeSet
|
|
8
|
-
|
|
16
|
+
changeSet: ChangeSet<T>;
|
|
17
|
+
resourceMetadata: ResourceConfig
|
|
9
18
|
|
|
10
|
-
constructor(id: string, changeSet: ChangeSet
|
|
19
|
+
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig) {
|
|
11
20
|
this.id = id;
|
|
12
21
|
this.changeSet = changeSet;
|
|
13
|
-
this.
|
|
22
|
+
this.resourceMetadata = resourceMetadata;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
static create<T extends
|
|
25
|
+
static create<T extends StringIndexedObject>(
|
|
26
|
+
desiredConfig: Partial<T> & ResourceConfig,
|
|
27
|
+
currentConfig: Partial<T> & ResourceConfig | null,
|
|
28
|
+
configuration: PlanConfiguration<T>
|
|
29
|
+
): Plan<T> {
|
|
30
|
+
const parameterConfigurations = configuration.parameterConfigurations ?? {} as Record<keyof T, ParameterConfiguration>;
|
|
31
|
+
const statefulParameterNames = new Set(
|
|
32
|
+
[...Object.entries(parameterConfigurations)]
|
|
33
|
+
.filter(([k, v]) => v.isStatefulParameter)
|
|
34
|
+
.map(([k, v]) => k)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
38
|
+
const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// TODO: After adding in state files, need to calculate deletes here
|
|
42
|
+
// Where current config exists and state config exists but desired config doesn't
|
|
43
|
+
|
|
44
|
+
// Explanation: This calculates the change set of the parameters between the
|
|
45
|
+
// two configs and then passes it to ChangeSet to calculate the overall
|
|
46
|
+
// operation for the resource
|
|
47
|
+
const parameterChangeSet = ChangeSet.calculateParameterChangeSet(
|
|
48
|
+
desiredParameters,
|
|
49
|
+
currentParameters,
|
|
50
|
+
{ statefulMode: configuration.statefulMode, parameterConfigurations }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let resourceOperation: ResourceOperation;
|
|
54
|
+
if (!currentConfig && desiredConfig) {
|
|
55
|
+
resourceOperation = ResourceOperation.CREATE;
|
|
56
|
+
} else if (currentConfig && !desiredConfig) {
|
|
57
|
+
resourceOperation = ResourceOperation.DESTROY;
|
|
58
|
+
} else {
|
|
59
|
+
resourceOperation = parameterChangeSet
|
|
60
|
+
.filter((change) => change.operation !== ParameterOperation.NOOP)
|
|
61
|
+
.reduce((operation: ResourceOperation, curr: ParameterChange<T>) => {
|
|
62
|
+
let newOperation: ResourceOperation;
|
|
63
|
+
if (statefulParameterNames.has(curr.name)) {
|
|
64
|
+
newOperation = ResourceOperation.MODIFY // All stateful parameters are modify only
|
|
65
|
+
} else if (parameterConfigurations[curr.name]?.planOperation) {
|
|
66
|
+
newOperation = parameterConfigurations[curr.name].planOperation!;
|
|
67
|
+
} else {
|
|
68
|
+
newOperation = ResourceOperation.RECREATE; // Default to Re-create. Should handle the majority of use cases
|
|
69
|
+
}
|
|
70
|
+
return ChangeSet.combineResourceOperations(operation, newOperation);
|
|
71
|
+
}, ResourceOperation.NOOP);
|
|
72
|
+
}
|
|
73
|
+
|
|
17
74
|
return new Plan(
|
|
18
75
|
randomUUID(),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
76
|
+
new ChangeSet<T>(resourceOperation, parameterChangeSet),
|
|
77
|
+
resourceMetadata,
|
|
78
|
+
);
|
|
22
79
|
}
|
|
23
80
|
|
|
24
81
|
getResourceType(): string {
|
|
25
|
-
return this.
|
|
82
|
+
return this.resourceMetadata.type
|
|
26
83
|
}
|
|
27
84
|
|
|
28
|
-
static fromResponse(data: ApplyRequestData['plan']): Plan<
|
|
85
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T> {
|
|
29
86
|
if (!data) {
|
|
30
87
|
throw new Error('Data is empty');
|
|
31
88
|
}
|
|
32
89
|
|
|
33
90
|
return new Plan(
|
|
34
91
|
randomUUID(),
|
|
35
|
-
new ChangeSet(
|
|
92
|
+
new ChangeSet<T>(
|
|
36
93
|
data.operation,
|
|
37
94
|
data.parameters.map(value => ({
|
|
38
95
|
...value,
|
|
@@ -50,12 +107,26 @@ export class Plan<T extends ResourceConfig> {
|
|
|
50
107
|
);
|
|
51
108
|
}
|
|
52
109
|
|
|
110
|
+
get desiredConfig(): T {
|
|
111
|
+
return {
|
|
112
|
+
...this.resourceMetadata,
|
|
113
|
+
...this.changeSet.desiredParameters,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get currentConfig(): T {
|
|
118
|
+
return {
|
|
119
|
+
...this.resourceMetadata,
|
|
120
|
+
...this.changeSet.currentParameters,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
53
124
|
toResponse(): PlanResponseData {
|
|
54
125
|
return {
|
|
55
126
|
planId: this.id,
|
|
56
127
|
operation: this.changeSet.operation,
|
|
57
|
-
resourceName: this.
|
|
58
|
-
resourceType: this.
|
|
128
|
+
resourceName: this.resourceMetadata.name,
|
|
129
|
+
resourceType: this.resourceMetadata.type,
|
|
59
130
|
parameters: this.changeSet.parameterChanges,
|
|
60
131
|
}
|
|
61
132
|
}
|
package/src/entities/plugin.ts
CHANGED
|
@@ -27,30 +27,32 @@ export class Plugin {
|
|
|
27
27
|
return {
|
|
28
28
|
resourceDefinitions: [...this.resources.values()]
|
|
29
29
|
.map((r) => ({
|
|
30
|
-
type: r.
|
|
30
|
+
type: r.typeId,
|
|
31
31
|
dependencies: r.getDependencyTypeIds(),
|
|
32
32
|
}))
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
|
|
37
|
-
const
|
|
37
|
+
const validationResults = [];
|
|
38
38
|
for (const config of data.configs) {
|
|
39
39
|
if (!this.resources.has(config.type)) {
|
|
40
40
|
throw new Error(`Resource type not found: ${config.type}`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const validateResult = await this.resources.get(config.type)!.validate(config);
|
|
44
|
+
|
|
45
|
+
validationResults.push({
|
|
46
|
+
...validateResult,
|
|
47
|
+
resourceType: config.type,
|
|
48
|
+
resourceName: config.name,
|
|
49
|
+
});
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
await this.crossValidateResources(data.configs);
|
|
50
53
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
+
validationResults
|
|
55
|
+
};
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
async plan(data: PlanRequestData): Promise<PlanResponseData> {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { StatefulParameter, StatefulParameterConfiguration } from './stateful-parameter.js';
|
|
3
|
+
import { Plan } from './plan.js';
|
|
4
|
+
import { spy } from 'sinon';
|
|
5
|
+
import { ResourceOperation } from 'codify-schemas';
|
|
6
|
+
import { TestConfig, TestResource } from './resource.test.js';
|
|
7
|
+
|
|
8
|
+
class TestParameter extends StatefulParameter<TestConfig, string> {
|
|
9
|
+
constructor(configuration?: StatefulParameterConfiguration<TestConfig>) {
|
|
10
|
+
super(configuration ?? {
|
|
11
|
+
name: 'propA'
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
applyAdd(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
applyModify(newValue: string, previousValue: string, allowDeletes: boolean, plan: Plan<TestConfig>): Promise<void> {
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
applyRemove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
async refresh(): Promise<string | null> {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Resource parameters tests', () => {
|
|
30
|
+
it('supports the creation of stateful parameters', async () => {
|
|
31
|
+
|
|
32
|
+
const statefulParameter = new class extends TestParameter {
|
|
33
|
+
async refresh(): Promise<string | null> {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
39
|
+
|
|
40
|
+
const resource = new class extends TestResource {
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super({
|
|
44
|
+
type: 'resource',
|
|
45
|
+
statefulParameters: [statefulParameterSpy],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resourceSpy = spy(resource);
|
|
51
|
+
const result = await resourceSpy.apply(
|
|
52
|
+
Plan.create<TestConfig>(
|
|
53
|
+
{ type: 'resource', propA: 'a', propB: 0, propC: 'b' },
|
|
54
|
+
null,
|
|
55
|
+
{ statefulMode: false },
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(statefulParameterSpy.applyAdd.calledOnce).to.be.true;
|
|
60
|
+
expect(resourceSpy.applyCreate.calledOnce).to.be.true;
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('supports the modification of stateful parameters', async () => {
|
|
64
|
+
const statefulParameter = new class extends TestParameter {
|
|
65
|
+
async refresh(): Promise<string | null> {
|
|
66
|
+
return 'b';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
71
|
+
|
|
72
|
+
const resource = new class extends TestResource {
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
super({
|
|
76
|
+
type: 'resource',
|
|
77
|
+
statefulParameters: [statefulParameterSpy],
|
|
78
|
+
parameterConfigurations: {
|
|
79
|
+
propB: { planOperation: ResourceOperation.MODIFY },
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
85
|
+
return { propB: -1, propC: 'b' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const plan = await resource.plan({ type: 'resource', propA: 'a', propB: 0, propC: 'b' })
|
|
90
|
+
|
|
91
|
+
const resourceSpy = spy(resource);
|
|
92
|
+
const result = await resourceSpy.apply(plan);
|
|
93
|
+
|
|
94
|
+
expect(statefulParameterSpy.applyModify.calledOnce).to.be.true;
|
|
95
|
+
expect(resourceSpy.applyModify.calledOnce).to.be.true;
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('Filters array results in stateless mode to prevent modify from being called', async () => {
|
|
99
|
+
const statefulParameter = new class extends TestParameter {
|
|
100
|
+
async refresh(): Promise<any | null> {
|
|
101
|
+
return ['a', 'b', 'c', 'd']
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
106
|
+
|
|
107
|
+
const resource = new class extends TestResource {
|
|
108
|
+
constructor() {
|
|
109
|
+
super({
|
|
110
|
+
type: 'resource',
|
|
111
|
+
statefulParameters: [statefulParameterSpy],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const plan = await resource.plan({ type: 'resource', propA: ['a', 'b'] } as any)
|
|
121
|
+
|
|
122
|
+
expect(plan).toMatchObject({
|
|
123
|
+
changeSet: {
|
|
124
|
+
operation: ResourceOperation.NOOP,
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('Filters array results in stateless mode to prevent modify from being called', async () => {
|
|
130
|
+
const statefulParameter = new class extends TestParameter {
|
|
131
|
+
async refresh(): Promise<any | null> {
|
|
132
|
+
return ['a', 'b']
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
137
|
+
|
|
138
|
+
const resource = new class extends TestResource {
|
|
139
|
+
constructor() {
|
|
140
|
+
super({
|
|
141
|
+
type: 'resource',
|
|
142
|
+
statefulParameters: [statefulParameterSpy],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const plan = await resource.plan({ type: 'resource', propA: ['a', 'b', 'c', 'd'] } as any)
|
|
152
|
+
|
|
153
|
+
expect(plan).toMatchObject({
|
|
154
|
+
changeSet: {
|
|
155
|
+
operation: ResourceOperation.MODIFY,
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { StatefulParameter } from './stateful-parameter.js';
|
|
2
|
+
import { ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
import { Resource } from './resource.js';
|
|
4
|
+
|
|
5
|
+
export type ErrorMessage = string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Customize properties for specific parameters. This will alter the way the library process changes to the parameter.
|
|
9
|
+
*/
|
|
10
|
+
export interface ResourceParameterConfiguration {
|
|
11
|
+
/**
|
|
12
|
+
* Chose if the resource should be re-created or modified if this parameter is changed. Defaults to re-create.
|
|
13
|
+
*/
|
|
14
|
+
planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
|
|
15
|
+
/**
|
|
16
|
+
* Customize the equality comparison for a parameter.
|
|
17
|
+
* @param a
|
|
18
|
+
* @param b
|
|
19
|
+
*/
|
|
20
|
+
isEqual?: (a: any, b: any) => boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param
|
|
25
|
+
*/
|
|
26
|
+
export interface ResourceConfiguration<T extends StringIndexedObject> {
|
|
27
|
+
type: string;
|
|
28
|
+
/**
|
|
29
|
+
* If true, statefulParameter.applyRemove() will be called before resource destruction.
|
|
30
|
+
* Defaults to false.
|
|
31
|
+
*/
|
|
32
|
+
callStatefulParameterRemoveOnDestroy?: boolean,
|
|
33
|
+
dependencies?: Resource<any>[];
|
|
34
|
+
statefulParameters?: Array<StatefulParameter<T, T[keyof T]>>;
|
|
35
|
+
parameterConfigurations?: Partial<Record<keyof T, ResourceParameterConfiguration>>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ResourceDefinition {
|
|
39
|
+
[x: string]: {
|
|
40
|
+
type: string;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ValidationResult {
|
|
45
|
+
isValid: boolean;
|
|
46
|
+
errors?: unknown[],
|
|
47
|
+
}
|