codify-plugin-lib 1.0.79 → 1.0.80
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/plan/plan.js +8 -7
- package/dist/utils/utils.d.ts +6 -1
- package/dist/utils/utils.js +43 -2
- package/package.json +1 -1
- package/src/plan/change-set.test.ts +27 -27
- package/src/plan/plan.test.ts +5 -4
- package/src/plan/plan.ts +15 -10
- package/src/resource/resource-controller.test.ts +3 -3
- package/src/resource/resource-settings.test.ts +32 -1
- package/src/resource/stateful-parameter.test.ts +35 -6
- package/src/utils/utils.ts +57 -2
package/dist/plan/plan.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
|
|
2
2
|
import { v4 as uuidV4 } from 'uuid';
|
|
3
|
+
import { asyncFilter, asyncIncludes, asyncMap } from '../utils/utils.js';
|
|
3
4
|
import { ChangeSet } from './change-set.js';
|
|
4
5
|
/**
|
|
5
6
|
* A plan represents a set of actions that after taken will turn the current resource into the desired one.
|
|
@@ -79,7 +80,7 @@ export class Plan {
|
|
|
79
80
|
settings,
|
|
80
81
|
statefulMode
|
|
81
82
|
});
|
|
82
|
-
const filteredCurrentParameters = Plan.filterCurrentParams({
|
|
83
|
+
const filteredCurrentParameters = await Plan.filterCurrentParams({
|
|
83
84
|
desiredParameters,
|
|
84
85
|
currentParameters,
|
|
85
86
|
stateParameters,
|
|
@@ -110,7 +111,7 @@ export class Plan {
|
|
|
110
111
|
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
111
112
|
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
112
113
|
*/
|
|
113
|
-
static filterCurrentParams(params) {
|
|
114
|
+
static async filterCurrentParams(params) {
|
|
114
115
|
const { desiredParameters: desired, currentParameters: current, stateParameters: state, settings, statefulMode } = params;
|
|
115
116
|
if (!current) {
|
|
116
117
|
return null;
|
|
@@ -125,9 +126,8 @@ export class Plan {
|
|
|
125
126
|
return filteredCurrent;
|
|
126
127
|
}
|
|
127
128
|
// TODO: Add object handling here in addition to arrays in the future
|
|
128
|
-
const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
|
|
129
|
-
.filter(([k, v]) => isArrayStatefulParameter(k, v))
|
|
130
|
-
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
|
|
129
|
+
const arrayStatefulParameters = Object.fromEntries(await asyncMap(Object.entries(filteredCurrent)
|
|
130
|
+
.filter(([k, v]) => isArrayStatefulParameter(k, v)), async ([k, v]) => [k, await filterArrayStatefulParameter(k, v)]));
|
|
131
131
|
return { ...filteredCurrent, ...arrayStatefulParameters };
|
|
132
132
|
function filterCurrent() {
|
|
133
133
|
if (!current) {
|
|
@@ -148,13 +148,14 @@ export class Plan {
|
|
|
148
148
|
&& settings.parameterSettings[k].definition.getSettings().type === 'array'
|
|
149
149
|
&& Array.isArray(v);
|
|
150
150
|
}
|
|
151
|
-
function filterArrayStatefulParameter(k, v) {
|
|
151
|
+
async function filterArrayStatefulParameter(k, v) {
|
|
152
152
|
const desiredArray = desired[k];
|
|
153
153
|
const matcher = settings.parameterSettings[k]
|
|
154
154
|
.definition
|
|
155
155
|
.getSettings()
|
|
156
156
|
.isElementEqual;
|
|
157
|
-
|
|
157
|
+
const eq = matcher ?? ((a, b) => a === b);
|
|
158
|
+
return asyncFilter(v, async (cv) => asyncIncludes(desiredArray, async (dv) => eq(dv, cv)));
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -37,5 +37,10 @@ export declare function splitUserConfig<T extends StringIndexedObject>(config: R
|
|
|
37
37
|
};
|
|
38
38
|
export declare function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean;
|
|
39
39
|
export declare function untildify(pathWithTilde: string): string;
|
|
40
|
-
export declare function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown): boolean
|
|
40
|
+
export declare function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown): Promise<boolean>;
|
|
41
|
+
export declare function asyncFilter<T>(arr: T[], filter: (a: T) => Promise<boolean> | boolean): Promise<T[]>;
|
|
42
|
+
export declare function asyncMap<T, R>(arr: T[], map: (a: T) => Promise<R> | R): Promise<R[]>;
|
|
43
|
+
export declare function asyncFindIndex<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<number>;
|
|
44
|
+
export declare function asyncFind<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<T | undefined>;
|
|
45
|
+
export declare function asyncIncludes<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<boolean>;
|
|
41
46
|
export {};
|
package/dist/utils/utils.js
CHANGED
|
@@ -72,7 +72,7 @@ const homeDirectory = os.homedir();
|
|
|
72
72
|
export function untildify(pathWithTilde) {
|
|
73
73
|
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
74
74
|
}
|
|
75
|
-
export function areArraysEqual(parameter, desired, current) {
|
|
75
|
+
export async function areArraysEqual(parameter, desired, current) {
|
|
76
76
|
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
77
77
|
throw new Error(`A non-array value:
|
|
78
78
|
|
|
@@ -88,10 +88,11 @@ Was provided even though type array was specified.
|
|
|
88
88
|
}
|
|
89
89
|
const desiredCopy = [...desired];
|
|
90
90
|
const currentCopy = [...current];
|
|
91
|
+
const eq = parameter.isElementEqual ?? ((a, b) => a === b);
|
|
91
92
|
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
92
93
|
// an isElementEqual method. Time: O(n^2)
|
|
93
94
|
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
94
|
-
const idx =
|
|
95
|
+
const idx = await asyncFindIndex(currentCopy, (e2) => eq(desiredCopy[counter], e2));
|
|
95
96
|
if (idx === -1) {
|
|
96
97
|
return false;
|
|
97
98
|
}
|
|
@@ -100,3 +101,43 @@ Was provided even though type array was specified.
|
|
|
100
101
|
}
|
|
101
102
|
return currentCopy.length === 0;
|
|
102
103
|
}
|
|
104
|
+
export async function asyncFilter(arr, filter) {
|
|
105
|
+
const result = [];
|
|
106
|
+
for (const element of arr) {
|
|
107
|
+
if (await filter(element)) {
|
|
108
|
+
result.push(element);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
export async function asyncMap(arr, map) {
|
|
114
|
+
const result = [];
|
|
115
|
+
for (const element of arr) {
|
|
116
|
+
result.push(await map(element));
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
export async function asyncFindIndex(arr, eq) {
|
|
121
|
+
for (const [counter, element] of arr.entries()) {
|
|
122
|
+
if (await eq(element)) {
|
|
123
|
+
return counter;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return -1;
|
|
127
|
+
}
|
|
128
|
+
export async function asyncFind(arr, eq) {
|
|
129
|
+
for (const element of arr) {
|
|
130
|
+
if (await eq(element)) {
|
|
131
|
+
return element;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
export async function asyncIncludes(arr, eq) {
|
|
137
|
+
for (const element of arr) {
|
|
138
|
+
if (await eq(element)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@ import { ChangeSet } from './change-set.js';
|
|
|
2
2
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
|
-
describe('Change set tests', () => {
|
|
6
|
-
it ('Correctly diffs two resource configs (modify)', () => {
|
|
5
|
+
describe('Change set tests', async () => {
|
|
6
|
+
it ('Correctly diffs two resource configs (modify)', async () => {
|
|
7
7
|
const after = {
|
|
8
8
|
propA: 'before',
|
|
9
9
|
propB: 'before'
|
|
@@ -14,14 +14,14 @@ describe('Change set tests', () => {
|
|
|
14
14
|
propB: 'after'
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const cs = ChangeSet.calculateModification(after, before);
|
|
17
|
+
const cs = await ChangeSet.calculateModification(after, before);
|
|
18
18
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
19
19
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
20
20
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.MODIFY);
|
|
21
21
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
it ('Correctly diffs two resource configs (add)', () => {
|
|
24
|
+
it ('Correctly diffs two resource configs (add)', async () => {
|
|
25
25
|
const after = {
|
|
26
26
|
propA: 'before',
|
|
27
27
|
propB: 'after'
|
|
@@ -31,7 +31,7 @@ describe('Change set tests', () => {
|
|
|
31
31
|
propA: 'after',
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const cs = ChangeSet.calculateModification(after, before,);
|
|
34
|
+
const cs = await ChangeSet.calculateModification(after, before,);
|
|
35
35
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
36
36
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
37
37
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.ADD);
|
|
@@ -39,7 +39,7 @@ describe('Change set tests', () => {
|
|
|
39
39
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
it ('Correctly diffs two resource configs (remove)', () => {
|
|
42
|
+
it ('Correctly diffs two resource configs (remove)', async () => {
|
|
43
43
|
const after = {
|
|
44
44
|
propA: 'after',
|
|
45
45
|
}
|
|
@@ -49,14 +49,14 @@ describe('Change set tests', () => {
|
|
|
49
49
|
propB: 'before'
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const cs = ChangeSet.calculateModification(after, before);
|
|
52
|
+
const cs = await ChangeSet.calculateModification(after, before);
|
|
53
53
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
54
54
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
55
55
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
56
56
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
it ('Correctly diffs two resource configs (no-op)', () => {
|
|
59
|
+
it ('Correctly diffs two resource configs (no-op)', async () => {
|
|
60
60
|
const after = {
|
|
61
61
|
propA: 'prop',
|
|
62
62
|
}
|
|
@@ -65,7 +65,7 @@ describe('Change set tests', () => {
|
|
|
65
65
|
propA: 'prop',
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const cs = ChangeSet.calculateModification(after, before);
|
|
68
|
+
const cs = await ChangeSet.calculateModification(after, before);
|
|
69
69
|
expect(cs.parameterChanges.length).to.eq(1);
|
|
70
70
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
71
71
|
expect(cs.operation).to.eq(ResourceOperation.NOOP)
|
|
@@ -95,7 +95,7 @@ describe('Change set tests', () => {
|
|
|
95
95
|
expect(cs.operation).to.eq(ResourceOperation.DESTROY)
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it ('handles simple arrays', () => {
|
|
98
|
+
it ('handles simple arrays', async () => {
|
|
99
99
|
const before = {
|
|
100
100
|
propA: ['a', 'b', 'c'],
|
|
101
101
|
}
|
|
@@ -104,13 +104,13 @@ describe('Change set tests', () => {
|
|
|
104
104
|
propA: ['b', 'a', 'c'],
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const cs = ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
|
|
107
|
+
const cs = await ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
|
|
108
108
|
expect(cs.parameterChanges.length).to.eq(1);
|
|
109
109
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
110
110
|
expect(cs.operation).to.eq(ResourceOperation.NOOP)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
it('handles simple arrays 2', () => {
|
|
113
|
+
it('handles simple arrays 2', async () => {
|
|
114
114
|
const after = {
|
|
115
115
|
propA: ['a', 'b', 'c'],
|
|
116
116
|
}
|
|
@@ -119,13 +119,13 @@ describe('Change set tests', () => {
|
|
|
119
119
|
propA: ['b', 'a'],
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const cs = ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
|
|
122
|
+
const cs = await ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
|
|
123
123
|
expect(cs.parameterChanges.length).to.eq(1);
|
|
124
124
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
125
125
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
-
it('determines the order of operations with canModify 1', () => {
|
|
128
|
+
it('determines the order of operations with canModify 1', async () => {
|
|
129
129
|
const after = {
|
|
130
130
|
propA: 'after',
|
|
131
131
|
}
|
|
@@ -135,14 +135,14 @@ describe('Change set tests', () => {
|
|
|
135
135
|
propB: 'before'
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const cs = ChangeSet.calculateModification(after, before, { propA: { canModify: true } });
|
|
138
|
+
const cs = await ChangeSet.calculateModification(after, before, { propA: { canModify: true } });
|
|
139
139
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
140
140
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
141
141
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
142
142
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
it('determines the order of operations with canModify 2', () => {
|
|
145
|
+
it('determines the order of operations with canModify 2', async () => {
|
|
146
146
|
const after = {
|
|
147
147
|
propA: 'after',
|
|
148
148
|
}
|
|
@@ -152,7 +152,7 @@ describe('Change set tests', () => {
|
|
|
152
152
|
propB: 'before'
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
const cs = ChangeSet.calculateModification<any>(after, before, {
|
|
155
|
+
const cs = await ChangeSet.calculateModification<any>(after, before, {
|
|
156
156
|
propA: { canModify: true },
|
|
157
157
|
propB: { canModify: true }
|
|
158
158
|
});
|
|
@@ -163,38 +163,38 @@ describe('Change set tests', () => {
|
|
|
163
163
|
})
|
|
164
164
|
|
|
165
165
|
|
|
166
|
-
it('correctly determines array equality', () => {
|
|
166
|
+
it('correctly determines array equality', async () => {
|
|
167
167
|
const arrA = ['a', 'b', 'd'];
|
|
168
168
|
const arrB = ['a', 'b', 'd'];
|
|
169
169
|
|
|
170
|
-
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
170
|
+
const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
171
171
|
|
|
172
172
|
expect(result.operation).to.eq(ResourceOperation.NOOP);
|
|
173
173
|
})
|
|
174
174
|
|
|
175
|
-
it('correctly determines array equality 2', () => {
|
|
175
|
+
it('correctly determines array equality 2', async () => {
|
|
176
176
|
const arrA = ['a', 'b'];
|
|
177
177
|
const arrB = ['a', 'b', 'd'];
|
|
178
178
|
|
|
179
|
-
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
179
|
+
const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
180
180
|
|
|
181
181
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
182
182
|
})
|
|
183
183
|
|
|
184
|
-
it('correctly determines array equality 3', () => {
|
|
184
|
+
it('correctly determines array equality 3', async () => {
|
|
185
185
|
const arrA = ['b', 'a', 'd'];
|
|
186
186
|
const arrB = ['a', 'b', 'd'];
|
|
187
187
|
|
|
188
|
-
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
188
|
+
const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
189
189
|
|
|
190
190
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
-
it('correctly determines array equality 4', () => {
|
|
193
|
+
it('correctly determines array equality 4', async () => {
|
|
194
194
|
const arrA = [{ key1: 'a' }, { key1: 'a' }, { key1: 'a' }];
|
|
195
195
|
const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
|
|
196
196
|
|
|
197
|
-
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
|
|
197
|
+
const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
|
|
198
198
|
propA: {
|
|
199
199
|
type: 'array',
|
|
200
200
|
isElementEqual: (a, b) => a.key1 === b.key1
|
|
@@ -204,11 +204,11 @@ describe('Change set tests', () => {
|
|
|
204
204
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
205
205
|
})
|
|
206
206
|
|
|
207
|
-
it('correctly determines array equality 5', () => {
|
|
207
|
+
it('correctly determines array equality 5', async () => {
|
|
208
208
|
const arrA = [{ key1: 'b' }, { key1: 'a' }, { key1: 'a' }];
|
|
209
209
|
const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
|
|
210
210
|
|
|
211
|
-
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
|
|
211
|
+
const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
|
|
212
212
|
propA: {
|
|
213
213
|
type: 'array',
|
|
214
214
|
isElementEqual: (a, b) => a.key1 === b.key1
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Plan } from './plan.js';
|
|
3
3
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
4
|
-
import { TestConfig, TestResource } from '../utils/test-utils.test.js';
|
|
4
|
+
import { TestConfig, testPlan, TestResource } from '../utils/test-utils.test.js';
|
|
5
5
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
6
6
|
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
7
7
|
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
@@ -128,8 +128,8 @@ describe('Plan entity tests', () => {
|
|
|
128
128
|
).to.be.true;
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
it('Returns the original resource names', () => {
|
|
132
|
-
const plan = Plan.calculate<TestConfig>({
|
|
131
|
+
it('Returns the original resource names', async () => {
|
|
132
|
+
const plan = await Plan.calculate<TestConfig>({
|
|
133
133
|
desiredParameters: { propA: 'propA' },
|
|
134
134
|
currentParametersArray: [{ propA: 'propA2' }],
|
|
135
135
|
stateParameters: null,
|
|
@@ -147,7 +147,8 @@ describe('Plan entity tests', () => {
|
|
|
147
147
|
operation: ResourceOperation.RECREATE
|
|
148
148
|
})
|
|
149
149
|
})
|
|
150
|
-
})
|
|
150
|
+
});
|
|
151
|
+
|
|
151
152
|
|
|
152
153
|
function createTestResource() {
|
|
153
154
|
return new class extends TestResource {
|
package/src/plan/plan.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { v4 as uuidV4 } from 'uuid';
|
|
|
10
10
|
|
|
11
11
|
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
12
12
|
import { ArrayParameterSetting, ResourceSettings, StatefulParameterSetting } from '../resource/resource-settings.js';
|
|
13
|
+
import { asyncFilter, asyncIncludes, asyncMap } from '../utils/utils.js';
|
|
13
14
|
import { ChangeSet } from './change-set.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -130,7 +131,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
130
131
|
statefulMode
|
|
131
132
|
});
|
|
132
133
|
|
|
133
|
-
const filteredCurrentParameters = Plan.filterCurrentParams<T>({
|
|
134
|
+
const filteredCurrentParameters = await Plan.filterCurrentParams<T>({
|
|
134
135
|
desiredParameters,
|
|
135
136
|
currentParameters,
|
|
136
137
|
stateParameters,
|
|
@@ -187,13 +188,13 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
187
188
|
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
188
189
|
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
189
190
|
*/
|
|
190
|
-
private static filterCurrentParams<T extends StringIndexedObject>(params: {
|
|
191
|
+
private static async filterCurrentParams<T extends StringIndexedObject>(params: {
|
|
191
192
|
desiredParameters: Partial<T> | null,
|
|
192
193
|
currentParameters: Partial<T> | null,
|
|
193
194
|
stateParameters: Partial<T> | null,
|
|
194
195
|
settings: ResourceSettings<T>,
|
|
195
196
|
statefulMode: boolean,
|
|
196
|
-
}): Partial<T> | null {
|
|
197
|
+
}): Promise<Partial<T> | null> {
|
|
197
198
|
const {
|
|
198
199
|
desiredParameters: desired,
|
|
199
200
|
currentParameters: current,
|
|
@@ -219,10 +220,12 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
219
220
|
|
|
220
221
|
// TODO: Add object handling here in addition to arrays in the future
|
|
221
222
|
const arrayStatefulParameters = Object.fromEntries(
|
|
222
|
-
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
await asyncMap(
|
|
224
|
+
Object.entries(filteredCurrent)
|
|
225
|
+
.filter(([k, v]) => isArrayStatefulParameter(k, v)),
|
|
226
|
+
async ([k, v]) => [k, await filterArrayStatefulParameter(k, v)],
|
|
227
|
+
)
|
|
228
|
+
);
|
|
226
229
|
|
|
227
230
|
return { ...filteredCurrent, ...arrayStatefulParameters }
|
|
228
231
|
|
|
@@ -253,15 +256,17 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
253
256
|
&& Array.isArray(v)
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
|
|
259
|
+
async function filterArrayStatefulParameter(k: string, v: unknown[]): Promise<unknown[]> {
|
|
257
260
|
const desiredArray = desired![k] as unknown[];
|
|
258
261
|
const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
|
|
259
262
|
.definition
|
|
260
263
|
.getSettings() as ArrayParameterSetting)
|
|
261
264
|
.isElementEqual;
|
|
262
265
|
|
|
263
|
-
|
|
264
|
-
|
|
266
|
+
const eq = matcher ?? ((a: unknown, b: unknown) => a === b)
|
|
267
|
+
|
|
268
|
+
return asyncFilter(v, async (cv) =>
|
|
269
|
+
asyncIncludes(desiredArray, async (dv) => eq(dv, cv))
|
|
265
270
|
)
|
|
266
271
|
}
|
|
267
272
|
}
|
|
@@ -99,7 +99,7 @@ describe('Resource tests', () => {
|
|
|
99
99
|
const resourceSpy = spy(resource);
|
|
100
100
|
|
|
101
101
|
await controllerSpy.apply(
|
|
102
|
-
testPlan({
|
|
102
|
+
await testPlan({
|
|
103
103
|
desired: { propA: 'a', propB: 0 },
|
|
104
104
|
})
|
|
105
105
|
)
|
|
@@ -116,7 +116,7 @@ describe('Resource tests', () => {
|
|
|
116
116
|
const resourceSpy = spy(resource);
|
|
117
117
|
|
|
118
118
|
await controllerSpy.apply(
|
|
119
|
-
testPlan({
|
|
119
|
+
await testPlan({
|
|
120
120
|
current: [{ propA: 'a', propB: 0 }],
|
|
121
121
|
state: { propA: 'a', propB: 0 },
|
|
122
122
|
statefulMode: true,
|
|
@@ -135,7 +135,7 @@ describe('Resource tests', () => {
|
|
|
135
135
|
const resourceSpy = spy(resource);
|
|
136
136
|
|
|
137
137
|
await controllerSpy.apply(
|
|
138
|
-
testPlan({
|
|
138
|
+
await testPlan({
|
|
139
139
|
desired: { propA: 'a', propB: 0 },
|
|
140
140
|
current: [{ propA: 'b', propB: -1 }],
|
|
141
141
|
statefulMode: true
|
|
@@ -77,7 +77,7 @@ describe('Resource parameter tests', () => {
|
|
|
77
77
|
const resourceSpy = spy(resource);
|
|
78
78
|
|
|
79
79
|
await controller.apply(
|
|
80
|
-
testPlan<TestConfig>({
|
|
80
|
+
await testPlan<TestConfig>({
|
|
81
81
|
desired: { propA: 'a', propB: 0, propC: 'c' }
|
|
82
82
|
})
|
|
83
83
|
);
|
|
@@ -491,4 +491,35 @@ describe('Resource parameter tests', () => {
|
|
|
491
491
|
|
|
492
492
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
493
493
|
})
|
|
494
|
+
|
|
495
|
+
it('Works with async equals methods', async () => {
|
|
496
|
+
const resource = new class extends TestResource {
|
|
497
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
498
|
+
return {
|
|
499
|
+
id: 'type',
|
|
500
|
+
parameterSettings: {
|
|
501
|
+
propA: {
|
|
502
|
+
isEqual: async (desired, current) => {
|
|
503
|
+
console.log(desired, current)
|
|
504
|
+
await sleep(500);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const controller = new ResourceController(resource);
|
|
513
|
+
|
|
514
|
+
const plan = await controller.plan({ type: 'type', propA: 'abc' } as any);
|
|
515
|
+
|
|
516
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
517
|
+
|
|
518
|
+
expect(plan.toResponse().operation).to.equal(ResourceOperation.NOOP);
|
|
519
|
+
})
|
|
494
520
|
})
|
|
521
|
+
|
|
522
|
+
function sleep(ms: number) {
|
|
523
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
524
|
+
}
|
|
525
|
+
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { spy } from 'sinon';
|
|
3
3
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
4
|
-
import { TestArrayStatefulParameter, TestConfig, testPlan } from '../utils/test-utils.test.js';
|
|
5
|
-
import { ArrayParameterSetting } from './resource-settings.js';
|
|
4
|
+
import { TestArrayStatefulParameter, TestConfig, testPlan, TestResource } from '../utils/test-utils.test.js';
|
|
5
|
+
import { ArrayParameterSetting, ResourceSettings } from './resource-settings.js';
|
|
6
|
+
import { ResourceController } from './resource-controller.js';
|
|
6
7
|
|
|
7
8
|
describe('Stateful parameter tests', () => {
|
|
8
9
|
it('addItem is called the correct number of times', async () => {
|
|
9
|
-
const plan = testPlan<TestConfig>({
|
|
10
|
+
const plan = await testPlan<TestConfig>({
|
|
10
11
|
desired: { propZ: ['a', 'b', 'c'] },
|
|
11
12
|
});
|
|
12
13
|
|
|
@@ -21,7 +22,7 @@ describe('Stateful parameter tests', () => {
|
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
it('applyRemoveItem is called the correct number of times', async () => {
|
|
24
|
-
const plan = testPlan<TestConfig>({
|
|
25
|
+
const plan = await testPlan<TestConfig>({
|
|
25
26
|
desired: null,
|
|
26
27
|
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
27
28
|
state: { propZ: ['a', 'b', 'c'] },
|
|
@@ -40,7 +41,7 @@ describe('Stateful parameter tests', () => {
|
|
|
40
41
|
|
|
41
42
|
it('In stateless mode only applyAddItem is called only for modifies', async () => {
|
|
42
43
|
const parameter = new TestArrayStatefulParameter()
|
|
43
|
-
const plan = testPlan<TestConfig>({
|
|
44
|
+
const plan = await testPlan<TestConfig>({
|
|
44
45
|
desired: { propZ: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
|
|
45
46
|
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
46
47
|
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: parameter } } },
|
|
@@ -71,7 +72,7 @@ describe('Stateful parameter tests', () => {
|
|
|
71
72
|
}
|
|
72
73
|
});
|
|
73
74
|
|
|
74
|
-
const plan = testPlan<TestConfig>({
|
|
75
|
+
const plan = await testPlan<TestConfig>({
|
|
75
76
|
desired: { propZ: ['9.12', '9.13'] }, // b to remove, d, e, f to add
|
|
76
77
|
current: [{ propZ: ['9.12.9'] }],
|
|
77
78
|
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
|
|
@@ -90,4 +91,32 @@ describe('Stateful parameter tests', () => {
|
|
|
90
91
|
expect(testParameter.addItem.calledOnce).to.be.true;
|
|
91
92
|
expect(testParameter.removeItem.called).to.be.false;
|
|
92
93
|
})
|
|
94
|
+
|
|
95
|
+
it('isElementEqual works being async', async () => {
|
|
96
|
+
const testParameter = spy(new class extends TestArrayStatefulParameter {
|
|
97
|
+
getSettings(): ArrayParameterSetting {
|
|
98
|
+
return {
|
|
99
|
+
type: 'array',
|
|
100
|
+
isElementEqual: async (desired, current) => {
|
|
101
|
+
console.log(desired, current)
|
|
102
|
+
await sleep(50);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const plan = await testPlan<TestConfig>({
|
|
110
|
+
desired: { propZ: ['9.12'] }, // b to remove, d, e, f to add
|
|
111
|
+
current: [{ propZ: ['23472934'] }], // purposely make these two values very different since isElementEqual always returns true
|
|
112
|
+
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(plan.toResponse().operation).to.equal(ResourceOperation.NOOP);
|
|
116
|
+
})
|
|
93
117
|
})
|
|
118
|
+
|
|
119
|
+
function sleep(ms: number) {
|
|
120
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
121
|
+
}
|
|
122
|
+
|
package/src/utils/utils.ts
CHANGED
|
@@ -111,7 +111,7 @@ export function untildify(pathWithTilde: string) {
|
|
|
111
111
|
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
|
|
114
|
+
export async function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
|
|
115
115
|
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
116
116
|
throw new Error(`A non-array value:
|
|
117
117
|
|
|
@@ -130,10 +130,12 @@ Was provided even though type array was specified.
|
|
|
130
130
|
const desiredCopy = [...desired];
|
|
131
131
|
const currentCopy = [...current];
|
|
132
132
|
|
|
133
|
+
const eq = parameter.isElementEqual ?? ((a, b) => a === b);
|
|
134
|
+
|
|
133
135
|
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
134
136
|
// an isElementEqual method. Time: O(n^2)
|
|
135
137
|
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
136
|
-
const idx =
|
|
138
|
+
const idx = await asyncFindIndex(currentCopy, (e2) => eq(desiredCopy[counter], e2))
|
|
137
139
|
|
|
138
140
|
if (idx === -1) {
|
|
139
141
|
return false;
|
|
@@ -145,3 +147,56 @@ Was provided even though type array was specified.
|
|
|
145
147
|
|
|
146
148
|
return currentCopy.length === 0;
|
|
147
149
|
}
|
|
150
|
+
|
|
151
|
+
export async function asyncFilter<T>(arr: T[], filter: (a: T) => Promise<boolean> | boolean): Promise<T[]> {
|
|
152
|
+
const result = [];
|
|
153
|
+
|
|
154
|
+
for (const element of arr) {
|
|
155
|
+
if (await filter(element)) {
|
|
156
|
+
result.push(element);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function asyncMap<T, R>(arr: T[], map: (a: T) => Promise<R> | R): Promise<R[]> {
|
|
164
|
+
const result: R[] = [];
|
|
165
|
+
|
|
166
|
+
for (const element of arr) {
|
|
167
|
+
result.push(await map(element));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
export async function asyncFindIndex<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<number> {
|
|
175
|
+
for (const [counter, element] of arr.entries()) {
|
|
176
|
+
if (await eq(element)) {
|
|
177
|
+
return counter;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return -1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function asyncFind<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<T | undefined> {
|
|
185
|
+
for (const element of arr) {
|
|
186
|
+
if (await eq(element)) {
|
|
187
|
+
return element;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function asyncIncludes<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<boolean> {
|
|
195
|
+
for (const element of arr) {
|
|
196
|
+
if (await eq(element)) {
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return false;
|
|
202
|
+
}
|