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.
@@ -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,
@@ -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();
@@ -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
- return v.filter((cv) => desiredArray.find((dv) => (matcher ?? ((a, b) => a === b))(dv, cv)));
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
@@ -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 {};
@@ -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 = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -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
@@ -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()
@@ -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
- Object.entries(filteredCurrent)
223
- .filter(([k, v]) => isArrayStatefulParameter(k, v))
224
- .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
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
- return v.filter((cv) =>
264
- desiredArray.find((dv) => (matcher ?? ((a: any, b: any) => a === b))(dv, cv))
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
+
@@ -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 = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2))
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
+ }