codify-plugin-lib 1.0.78 → 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/change-set.d.ts +1 -1
- package/dist/plan/change-set.js +5 -5
- package/dist/plan/plan.d.ts +1 -1
- package/dist/plan/plan.js +10 -9
- package/dist/resource/resource-settings.d.ts +2 -2
- 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/change-set.ts +8 -8
- package/src/plan/plan.test.ts +5 -4
- package/src/plan/plan.ts +18 -13
- package/src/resource/parsed-resource-settings.ts +1 -1
- package/src/resource/resource-controller.test.ts +3 -3
- package/src/resource/resource-settings.test.ts +32 -1
- package/src/resource/resource-settings.ts +2 -2
- package/src/resource/stateful-parameter.test.ts +35 -6
- package/src/utils/utils.ts +57 -2
|
@@ -30,7 +30,7 @@ export declare class ChangeSet<T extends StringIndexedObject> {
|
|
|
30
30
|
static empty<T extends StringIndexedObject>(): ChangeSet<T>;
|
|
31
31
|
static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T>;
|
|
32
32
|
static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T>;
|
|
33
|
-
static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): ChangeSet<T
|
|
33
|
+
static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): Promise<ChangeSet<T>>;
|
|
34
34
|
/**
|
|
35
35
|
* Calculates the differences between the desired and current parameters,
|
|
36
36
|
* and returns a list of parameter changes that describe what needs to be added,
|
package/dist/plan/change-set.js
CHANGED
|
@@ -45,8 +45,8 @@ export class ChangeSet {
|
|
|
45
45
|
}));
|
|
46
46
|
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
47
47
|
}
|
|
48
|
-
static calculateModification(desired, current, parameterSettings = {}) {
|
|
49
|
-
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
48
|
+
static async calculateModification(desired, current, parameterSettings = {}) {
|
|
49
|
+
const pc = await ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
50
50
|
const statefulParameterKeys = new Set(Object.entries(parameterSettings)
|
|
51
51
|
.filter(([, v]) => v?.type === 'stateful')
|
|
52
52
|
.map(([k]) => k));
|
|
@@ -77,7 +77,7 @@ export class ChangeSet {
|
|
|
77
77
|
* @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
|
|
78
78
|
* @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
|
|
79
79
|
*/
|
|
80
|
-
static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
|
|
80
|
+
static async calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
|
|
81
81
|
const parameterChangeSet = new Array();
|
|
82
82
|
// Filter out null and undefined values or else the diff below will not work
|
|
83
83
|
const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
|
|
@@ -93,7 +93,7 @@ export class ChangeSet {
|
|
|
93
93
|
delete current[k];
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
|
-
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
96
|
+
if (!await ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
97
97
|
parameterChangeSet.push({
|
|
98
98
|
name: k,
|
|
99
99
|
previousValue: v ?? null,
|
|
@@ -138,7 +138,7 @@ export class ChangeSet {
|
|
|
138
138
|
const indexNext = orderOfOperations.indexOf(next);
|
|
139
139
|
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
140
140
|
}
|
|
141
|
-
static isSame(desired, current, setting) {
|
|
141
|
+
static async isSame(desired, current, setting) {
|
|
142
142
|
switch (setting?.type) {
|
|
143
143
|
case 'stateful': {
|
|
144
144
|
const statefulSetting = setting.definition.getSettings();
|
package/dist/plan/plan.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
41
41
|
coreParameters: ResourceConfig;
|
|
42
42
|
settings: ParsedResourceSettings<T>;
|
|
43
43
|
statefulMode: boolean;
|
|
44
|
-
}): Plan<T
|
|
44
|
+
}): Promise<Plan<T>>;
|
|
45
45
|
/**
|
|
46
46
|
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
47
47
|
* defined.
|
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.
|
|
@@ -70,7 +71,7 @@ export class Plan {
|
|
|
70
71
|
getResourceType() {
|
|
71
72
|
return this.coreParameters.type;
|
|
72
73
|
}
|
|
73
|
-
static calculate(params) {
|
|
74
|
+
static async calculate(params) {
|
|
74
75
|
const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
|
|
75
76
|
const currentParameters = Plan.matchCurrentParameters({
|
|
76
77
|
desiredParameters,
|
|
@@ -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,
|
|
@@ -99,7 +100,7 @@ export class Plan {
|
|
|
99
100
|
return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
|
|
100
101
|
}
|
|
101
102
|
// NO-OP, MODIFY or RE-CREATE
|
|
102
|
-
const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
|
|
103
|
+
const changeSet = await ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
|
|
103
104
|
return new Plan(uuidV4(), changeSet, coreParameters);
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
@@ -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.
|
|
@@ -97,7 +97,7 @@ export interface DefaultParameterSetting {
|
|
|
97
97
|
*
|
|
98
98
|
* @return Return true if equal
|
|
99
99
|
*/
|
|
100
|
-
isEqual?: (desired: any, current: any) => boolean;
|
|
100
|
+
isEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
|
|
101
101
|
/**
|
|
102
102
|
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
103
103
|
* Defaults to false (re-create).
|
|
@@ -123,7 +123,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
123
123
|
*
|
|
124
124
|
* @return Return true if desired is equivalent to current.
|
|
125
125
|
*/
|
|
126
|
-
isElementEqual?: (desired: any, current: any) => boolean;
|
|
126
|
+
isElementEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
129
|
* Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
|
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/change-set.ts
CHANGED
|
@@ -85,12 +85,12 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
85
85
|
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
static calculateModification<T extends StringIndexedObject>(
|
|
88
|
+
static async calculateModification<T extends StringIndexedObject>(
|
|
89
89
|
desired: Partial<T>,
|
|
90
90
|
current: Partial<T>,
|
|
91
91
|
parameterSettings: Partial<Record<keyof T, ParameterSetting>> = {},
|
|
92
|
-
): ChangeSet<T
|
|
93
|
-
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
92
|
+
): Promise<ChangeSet<T>> {
|
|
93
|
+
const pc = await ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
94
94
|
|
|
95
95
|
const statefulParameterKeys = new Set(
|
|
96
96
|
Object.entries(parameterSettings)
|
|
@@ -126,11 +126,11 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
126
126
|
* @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
|
|
127
127
|
* @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
|
|
128
128
|
*/
|
|
129
|
-
private static calculateParameterChanges<T extends StringIndexedObject>(
|
|
129
|
+
private static async calculateParameterChanges<T extends StringIndexedObject>(
|
|
130
130
|
desiredParameters: Partial<T>,
|
|
131
131
|
currentParameters: Partial<T>,
|
|
132
132
|
parameterOptions?: Partial<Record<keyof T, ParameterSetting>>,
|
|
133
|
-
): ParameterChange<T>[] {
|
|
133
|
+
): Promise<ParameterChange<T>[]> {
|
|
134
134
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
135
135
|
|
|
136
136
|
// Filter out null and undefined values or else the diff below will not work
|
|
@@ -155,7 +155,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
155
155
|
continue;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
158
|
+
if (!await ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
159
159
|
parameterChangeSet.push({
|
|
160
160
|
name: k,
|
|
161
161
|
previousValue: v ?? null,
|
|
@@ -210,11 +210,11 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
210
210
|
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
private static isSame(
|
|
213
|
+
private static async isSame(
|
|
214
214
|
desired: unknown,
|
|
215
215
|
current: unknown,
|
|
216
216
|
setting?: ParameterSetting,
|
|
217
|
-
): boolean {
|
|
217
|
+
): Promise<boolean> {
|
|
218
218
|
switch (setting?.type) {
|
|
219
219
|
case 'stateful': {
|
|
220
220
|
const statefulSetting = (setting as StatefulParameterSetting).definition.getSettings()
|
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
|
/**
|
|
@@ -105,14 +106,14 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
105
106
|
return this.coreParameters.type
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
static calculate<T extends StringIndexedObject>(params: {
|
|
109
|
+
static async calculate<T extends StringIndexedObject>(params: {
|
|
109
110
|
desiredParameters: Partial<T> | null,
|
|
110
111
|
currentParametersArray: Partial<T>[] | null,
|
|
111
112
|
stateParameters: Partial<T> | null,
|
|
112
113
|
coreParameters: ResourceConfig,
|
|
113
114
|
settings: ParsedResourceSettings<T>,
|
|
114
115
|
statefulMode: boolean,
|
|
115
|
-
}): Plan<T
|
|
116
|
+
}): Promise<Plan<T>> {
|
|
116
117
|
const {
|
|
117
118
|
desiredParameters,
|
|
118
119
|
currentParametersArray,
|
|
@@ -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,
|
|
@@ -166,7 +167,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
// NO-OP, MODIFY or RE-CREATE
|
|
169
|
-
const changeSet = ChangeSet.calculateModification(
|
|
170
|
+
const changeSet = await ChangeSet.calculateModification(
|
|
170
171
|
desiredParameters!,
|
|
171
172
|
filteredCurrentParameters!,
|
|
172
173
|
settings.parameterSettings,
|
|
@@ -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
|
}
|
|
@@ -154,7 +154,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
154
154
|
// The rest of the types have defaults set already
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
private resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean {
|
|
157
|
+
private resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => Promise<boolean> | boolean {
|
|
158
158
|
if (parameter.type === 'array') {
|
|
159
159
|
return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
|
|
160
160
|
}
|
|
@@ -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
|
+
|
|
@@ -127,7 +127,7 @@ export interface DefaultParameterSetting {
|
|
|
127
127
|
*
|
|
128
128
|
* @return Return true if equal
|
|
129
129
|
*/
|
|
130
|
-
isEqual?: (desired: any, current: any) => boolean;
|
|
130
|
+
isEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
@@ -156,7 +156,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
156
156
|
*
|
|
157
157
|
* @return Return true if desired is equivalent to current.
|
|
158
158
|
*/
|
|
159
|
-
isElementEqual?: (desired: any, current: any) => boolean
|
|
159
|
+
isElementEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
@@ -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
|
+
}
|