codify-plugin-lib 1.0.80 → 1.0.82

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>>): Promise<ChangeSet<T>>;
33
+ static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): 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,
@@ -1,5 +1,4 @@
1
1
  import { ParameterOperation, ResourceOperation } from 'codify-schemas';
2
- import { areArraysEqual } from '../utils/utils.js';
3
2
  // Change set will coerce undefined values to null because undefined is not valid JSON
4
3
  export class ChangeSet {
5
4
  operation;
@@ -45,8 +44,8 @@ export class ChangeSet {
45
44
  }));
46
45
  return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
47
46
  }
48
- static async calculateModification(desired, current, parameterSettings = {}) {
49
- const pc = await ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
47
+ static calculateModification(desired, current, parameterSettings = {}) {
48
+ const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
50
49
  const statefulParameterKeys = new Set(Object.entries(parameterSettings)
51
50
  .filter(([, v]) => v?.type === 'stateful')
52
51
  .map(([k]) => k));
@@ -77,7 +76,7 @@ export class ChangeSet {
77
76
  * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
78
77
  * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
79
78
  */
80
- static async calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
79
+ static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
81
80
  const parameterChangeSet = new Array();
82
81
  // Filter out null and undefined values or else the diff below will not work
83
82
  const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
@@ -93,7 +92,7 @@ export class ChangeSet {
93
92
  delete current[k];
94
93
  continue;
95
94
  }
96
- if (!await ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
95
+ if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
97
96
  parameterChangeSet.push({
98
97
  name: k,
99
98
  previousValue: v ?? null,
@@ -138,19 +137,7 @@ export class ChangeSet {
138
137
  const indexNext = orderOfOperations.indexOf(next);
139
138
  return orderOfOperations[Math.max(indexPrev, indexNext)];
140
139
  }
141
- static async isSame(desired, current, setting) {
142
- switch (setting?.type) {
143
- case 'stateful': {
144
- const statefulSetting = setting.definition.getSettings();
145
- return ChangeSet.isSame(desired, current, statefulSetting);
146
- }
147
- case 'array': {
148
- const arrayParameter = setting;
149
- return areArraysEqual(arrayParameter, desired, current);
150
- }
151
- default: {
152
- return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
153
- }
154
- }
140
+ static isSame(desired, current, setting) {
141
+ return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
155
142
  }
156
143
  }
@@ -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
- }): Promise<Plan<T>>;
44
+ }): 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,6 +1,5 @@
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';
4
3
  import { ChangeSet } from './change-set.js';
5
4
  /**
6
5
  * A plan represents a set of actions that after taken will turn the current resource into the desired one.
@@ -71,7 +70,7 @@ export class Plan {
71
70
  getResourceType() {
72
71
  return this.coreParameters.type;
73
72
  }
74
- static async calculate(params) {
73
+ static calculate(params) {
75
74
  const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
76
75
  const currentParameters = Plan.matchCurrentParameters({
77
76
  desiredParameters,
@@ -80,7 +79,7 @@ export class Plan {
80
79
  settings,
81
80
  statefulMode
82
81
  });
83
- const filteredCurrentParameters = await Plan.filterCurrentParams({
82
+ const filteredCurrentParameters = Plan.filterCurrentParams({
84
83
  desiredParameters,
85
84
  currentParameters,
86
85
  stateParameters,
@@ -100,7 +99,7 @@ export class Plan {
100
99
  return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
101
100
  }
102
101
  // NO-OP, MODIFY or RE-CREATE
103
- const changeSet = await ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
102
+ const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
104
103
  return new Plan(uuidV4(), changeSet, coreParameters);
105
104
  }
106
105
  /**
@@ -111,7 +110,7 @@ export class Plan {
111
110
  * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
112
111
  * or wants to set. If a parameter is not specified then it's not managed by Codify.
113
112
  */
114
- static async filterCurrentParams(params) {
113
+ static filterCurrentParams(params) {
115
114
  const { desiredParameters: desired, currentParameters: current, stateParameters: state, settings, statefulMode } = params;
116
115
  if (!current) {
117
116
  return null;
@@ -126,8 +125,9 @@ export class Plan {
126
125
  return filteredCurrent;
127
126
  }
128
127
  // TODO: Add object handling here in addition to arrays in the future
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)]));
128
+ const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
129
+ .filter(([k, v]) => isArrayStatefulParameter(k, v))
130
+ .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
131
131
  return { ...filteredCurrent, ...arrayStatefulParameters };
132
132
  function filterCurrent() {
133
133
  if (!current) {
@@ -148,14 +148,26 @@ export class Plan {
148
148
  && settings.parameterSettings[k].definition.getSettings().type === 'array'
149
149
  && Array.isArray(v);
150
150
  }
151
- async function filterArrayStatefulParameter(k, v) {
151
+ // For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
152
+ function filterArrayStatefulParameter(k, v) {
152
153
  const desiredArray = desired[k];
153
154
  const matcher = settings.parameterSettings[k]
154
155
  .definition
155
156
  .getSettings()
156
- .isElementEqual;
157
- const eq = matcher ?? ((a, b) => a === b);
158
- return asyncFilter(v, async (cv) => asyncIncludes(desiredArray, async (dv) => eq(dv, cv)));
157
+ .isElementEqual ?? ((a, b) => a === b);
158
+ const desiredCopy = [...desiredArray];
159
+ const currentCopy = [...v];
160
+ const result = [];
161
+ for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
162
+ const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2));
163
+ if (idx === -1) {
164
+ continue;
165
+ }
166
+ desiredCopy.splice(counter, 1);
167
+ const [element] = currentCopy.splice(idx, 1);
168
+ result.push(element);
169
+ }
170
+ return result;
159
171
  }
160
172
  }
161
173
  // TODO: This needs to be revisited. I don't think this is valid anymore.
@@ -21,6 +21,5 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
21
21
  get statefulParameterOrder(): Map<keyof T, number>;
22
22
  private validateSettings;
23
23
  private validateParameterEqualsFn;
24
- private resolveEqualsFn;
25
24
  private getFromCacheOrCreate;
26
25
  }
@@ -1,5 +1,4 @@
1
- import { areArraysEqual } from '../utils/utils.js';
2
- import { ParameterEqualsDefaults } from './resource-settings.js';
1
+ import { resolveEqualsFn } from './resource-settings.js';
3
2
  export class ParsedResourceSettings {
4
3
  cache = new Map();
5
4
  id;
@@ -35,7 +34,7 @@ export class ParsedResourceSettings {
35
34
  const settings = Object.entries(this.settings.parameterSettings ?? {})
36
35
  .map(([k, v]) => [k, v])
37
36
  .map(([k, v]) => {
38
- v.isEqual = this.resolveEqualsFn(v, k);
37
+ v.isEqual = resolveEqualsFn(v, k);
39
38
  return [k, v];
40
39
  });
41
40
  return Object.fromEntries(settings);
@@ -106,15 +105,6 @@ export class ParsedResourceSettings {
106
105
  }
107
106
  // The rest of the types have defaults set already
108
107
  }
109
- resolveEqualsFn(parameter, key) {
110
- if (parameter.type === 'array') {
111
- return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter);
112
- }
113
- if (parameter.type === 'stateful') {
114
- return this.resolveEqualsFn(parameter.definition.getSettings(), key);
115
- }
116
- return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
117
- }
118
108
  getFromCacheOrCreate(key, create) {
119
109
  if (this.cache.has(key)) {
120
110
  return this.cache.get(key);
@@ -97,7 +97,7 @@ export interface DefaultParameterSetting {
97
97
  *
98
98
  * @return Return true if equal
99
99
  */
100
- isEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
100
+ isEqual?: (desired: any, current: any) => 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) => Promise<boolean> | boolean;
126
+ isElementEqual?: (desired: any, current: any) => boolean;
127
127
  }
128
128
  /**
129
129
  * Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
@@ -146,4 +146,4 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
146
146
  */
147
147
  order?: number;
148
148
  }
149
- export declare const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>>;
149
+ export declare function resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean;
@@ -1,9 +1,18 @@
1
1
  import path from 'node:path';
2
- import { untildify } from '../utils/utils.js';
3
- export const ParameterEqualsDefaults = {
2
+ import { areArraysEqual, untildify } from '../utils/utils.js';
3
+ const ParameterEqualsDefaults = {
4
4
  'boolean': (a, b) => Boolean(a) === Boolean(b),
5
5
  'directory': (a, b) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
6
6
  'number': (a, b) => Number(a) === Number(b),
7
7
  'string': (a, b) => String(a) === String(b),
8
8
  'version': (desired, current) => String(current).includes(String(desired))
9
9
  };
10
+ export function resolveEqualsFn(parameter, key) {
11
+ if (parameter.type === 'array') {
12
+ return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter);
13
+ }
14
+ if (parameter.type === 'stateful') {
15
+ return resolveEqualsFn(parameter.definition.getSettings(), key);
16
+ }
17
+ return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
18
+ }
@@ -37,10 +37,5 @@ 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): 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>;
40
+ export declare function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown): boolean;
46
41
  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 async function areArraysEqual(parameter, desired, current) {
75
+ export 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,11 +88,10 @@ 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);
92
91
  // Algorithm for to check equality between two un-ordered; un-hashable arrays using
93
92
  // an isElementEqual method. Time: O(n^2)
94
93
  for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
95
- const idx = await asyncFindIndex(currentCopy, (e2) => eq(desiredCopy[counter], e2));
94
+ const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2));
96
95
  if (idx === -1) {
97
96
  return false;
98
97
  }
@@ -101,43 +100,3 @@ Was provided even though type array was specified.
101
100
  }
102
101
  return currentCopy.length === 0;
103
102
  }
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.80",
3
+ "version": "1.0.82",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "@oclif/prettier-config": "^0.2.1",
23
23
  "@oclif/test": "^3",
24
24
  "@types/npmcli__promise-spawn": "^6.0.3",
25
- "@types/node": "^18",
25
+ "@types/node": "^20",
26
26
  "@types/semver": "^7.5.4",
27
27
  "@types/sinon": "^17.0.3",
28
28
  "@types/uuid": "^10.0.0",
@@ -1,9 +1,10 @@
1
1
  import { ChangeSet } from './change-set.js';
2
2
  import { ParameterOperation, ResourceOperation } from 'codify-schemas';
3
3
  import { describe, expect, it } from 'vitest';
4
+ import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
4
5
 
5
- describe('Change set tests', async () => {
6
- it ('Correctly diffs two resource configs (modify)', async () => {
6
+ describe('Change set tests', () => {
7
+ it ('Correctly diffs two resource configs (modify)', () => {
7
8
  const after = {
8
9
  propA: 'before',
9
10
  propB: 'before'
@@ -14,14 +15,14 @@ describe('Change set tests', async () => {
14
15
  propB: 'after'
15
16
  }
16
17
 
17
- const cs = await ChangeSet.calculateModification(after, before);
18
+ const cs = ChangeSet.calculateModification(after, before);
18
19
  expect(cs.parameterChanges.length).to.eq(2);
19
20
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
20
21
  expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.MODIFY);
21
22
  expect(cs.operation).to.eq(ResourceOperation.RECREATE)
22
23
  })
23
24
 
24
- it ('Correctly diffs two resource configs (add)', async () => {
25
+ it ('Correctly diffs two resource configs (add)', () => {
25
26
  const after = {
26
27
  propA: 'before',
27
28
  propB: 'after'
@@ -31,7 +32,7 @@ describe('Change set tests', async () => {
31
32
  propA: 'after',
32
33
  }
33
34
 
34
- const cs = await ChangeSet.calculateModification(after, before,);
35
+ const cs = ChangeSet.calculateModification(after, before);
35
36
  expect(cs.parameterChanges.length).to.eq(2);
36
37
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
37
38
  expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.ADD);
@@ -39,7 +40,7 @@ describe('Change set tests', async () => {
39
40
 
40
41
  })
41
42
 
42
- it ('Correctly diffs two resource configs (remove)', async () => {
43
+ it ('Correctly diffs two resource configs (remove)', () => {
43
44
  const after = {
44
45
  propA: 'after',
45
46
  }
@@ -49,14 +50,14 @@ describe('Change set tests', async () => {
49
50
  propB: 'before'
50
51
  }
51
52
 
52
- const cs = await ChangeSet.calculateModification(after, before);
53
+ const cs = ChangeSet.calculateModification(after, before);
53
54
  expect(cs.parameterChanges.length).to.eq(2);
54
55
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
55
56
  expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
56
57
  expect(cs.operation).to.eq(ResourceOperation.RECREATE)
57
58
  })
58
59
 
59
- it ('Correctly diffs two resource configs (no-op)', async () => {
60
+ it ('Correctly diffs two resource configs (no-op)', () => {
60
61
  const after = {
61
62
  propA: 'prop',
62
63
  }
@@ -65,7 +66,7 @@ describe('Change set tests', async () => {
65
66
  propA: 'prop',
66
67
  }
67
68
 
68
- const cs = await ChangeSet.calculateModification(after, before);
69
+ const cs = ChangeSet.calculateModification(after, before);
69
70
  expect(cs.parameterChanges.length).to.eq(1);
70
71
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
71
72
  expect(cs.operation).to.eq(ResourceOperation.NOOP)
@@ -95,7 +96,7 @@ describe('Change set tests', async () => {
95
96
  expect(cs.operation).to.eq(ResourceOperation.DESTROY)
96
97
  })
97
98
 
98
- it ('handles simple arrays', async () => {
99
+ it ('handles simple arrays', () => {
99
100
  const before = {
100
101
  propA: ['a', 'b', 'c'],
101
102
  }
@@ -104,13 +105,20 @@ describe('Change set tests', async () => {
104
105
  propA: ['b', 'a', 'c'],
105
106
  }
106
107
 
107
- const cs = await ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
108
+ const parameterSettings = new ParsedResourceSettings({
109
+ id: 'type',
110
+ parameterSettings: {
111
+ propA: { type: 'array' }
112
+ }
113
+ }).parameterSettings
114
+
115
+ const cs = ChangeSet.calculateModification(after, before, parameterSettings);
108
116
  expect(cs.parameterChanges.length).to.eq(1);
109
117
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
110
118
  expect(cs.operation).to.eq(ResourceOperation.NOOP)
111
119
  })
112
120
 
113
- it('handles simple arrays 2', async () => {
121
+ it('handles simple arrays 2', () => {
114
122
  const after = {
115
123
  propA: ['a', 'b', 'c'],
116
124
  }
@@ -119,13 +127,20 @@ describe('Change set tests', async () => {
119
127
  propA: ['b', 'a'],
120
128
  }
121
129
 
122
- const cs = await ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
130
+ const parameterSettings = new ParsedResourceSettings({
131
+ id: 'type',
132
+ parameterSettings: {
133
+ propA: { type: 'array' }
134
+ }
135
+ }).parameterSettings
136
+
137
+ const cs = ChangeSet.calculateModification(after, before, parameterSettings);
123
138
  expect(cs.parameterChanges.length).to.eq(1);
124
139
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
125
140
  expect(cs.operation).to.eq(ResourceOperation.RECREATE)
126
141
  })
127
142
 
128
- it('determines the order of operations with canModify 1', async () => {
143
+ it('determines the order of operations with canModify 1', () => {
129
144
  const after = {
130
145
  propA: 'after',
131
146
  }
@@ -135,14 +150,21 @@ describe('Change set tests', async () => {
135
150
  propB: 'before'
136
151
  }
137
152
 
138
- const cs = await ChangeSet.calculateModification(after, before, { propA: { canModify: true } });
153
+ const parameterSettings = new ParsedResourceSettings({
154
+ id: 'type',
155
+ parameterSettings: {
156
+ propA: { canModify: true }
157
+ }
158
+ }).parameterSettings
159
+
160
+ const cs = ChangeSet.calculateModification(after, before, parameterSettings);
139
161
  expect(cs.parameterChanges.length).to.eq(2);
140
162
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
141
163
  expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
142
164
  expect(cs.operation).to.eq(ResourceOperation.RECREATE)
143
165
  })
144
166
 
145
- it('determines the order of operations with canModify 2', async () => {
167
+ it('determines the order of operations with canModify 2', () => {
146
168
  const after = {
147
169
  propA: 'after',
148
170
  }
@@ -152,10 +174,15 @@ describe('Change set tests', async () => {
152
174
  propB: 'before'
153
175
  }
154
176
 
155
- const cs = await ChangeSet.calculateModification<any>(after, before, {
156
- propA: { canModify: true },
157
- propB: { canModify: true }
158
- });
177
+ const parameterSettings = new ParsedResourceSettings({
178
+ id: 'type',
179
+ parameterSettings: {
180
+ propA: { canModify: true },
181
+ propB: { canModify: true }
182
+ },
183
+ }).parameterSettings
184
+
185
+ const cs = ChangeSet.calculateModification<any>(after, before, parameterSettings);
159
186
  expect(cs.parameterChanges.length).to.eq(2);
160
187
  expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
161
188
  expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
@@ -163,57 +190,90 @@ describe('Change set tests', async () => {
163
190
  })
164
191
 
165
192
 
166
- it('correctly determines array equality', async () => {
193
+ it('correctly determines array equality', () => {
167
194
  const arrA = ['a', 'b', 'd'];
168
195
  const arrB = ['a', 'b', 'd'];
169
196
 
170
- const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
197
+ const parameterSettings = new ParsedResourceSettings({
198
+ id: 'type',
199
+ parameterSettings: {
200
+ propA: { type: 'array' }
201
+ },
202
+ }).parameterSettings
203
+
204
+
205
+ const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
171
206
 
172
207
  expect(result.operation).to.eq(ResourceOperation.NOOP);
173
208
  })
174
209
 
175
- it('correctly determines array equality 2', async () => {
210
+ it('correctly determines array equality 2', () => {
176
211
  const arrA = ['a', 'b'];
177
212
  const arrB = ['a', 'b', 'd'];
178
213
 
179
- const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
214
+ const parameterSettings = new ParsedResourceSettings({
215
+ id: 'type',
216
+ parameterSettings: {
217
+ propA: { type: 'array' }
218
+ },
219
+ }).parameterSettings
220
+
221
+ const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
180
222
 
181
223
  expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
182
224
  })
183
225
 
184
- it('correctly determines array equality 3', async () => {
226
+ it('correctly determines array equality 3', () => {
185
227
  const arrA = ['b', 'a', 'd'];
186
228
  const arrB = ['a', 'b', 'd'];
187
229
 
188
- const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
230
+ const parameterSettings = new ParsedResourceSettings({
231
+ id: 'type',
232
+ parameterSettings: {
233
+ propA: { type: 'array' }
234
+ },
235
+ }).parameterSettings
236
+
237
+ const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
189
238
 
190
239
  expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
191
240
  })
192
241
 
193
- it('correctly determines array equality 4', async () => {
242
+ it('correctly determines array equality 4', () => {
194
243
  const arrA = [{ key1: 'a' }, { key1: 'a' }, { key1: 'a' }];
195
244
  const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
196
245
 
197
- const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
198
- propA: {
199
- type: 'array',
200
- isElementEqual: (a, b) => a.key1 === b.key1
201
- }
202
- })
246
+ const parameterSettings = new ParsedResourceSettings({
247
+ id: 'type',
248
+ parameterSettings: {
249
+ propA: {
250
+ type: 'array',
251
+ isElementEqual: (a, b) => a.key1 === b.key1
252
+ }
253
+ },
254
+ }).parameterSettings
255
+
256
+
257
+ const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
203
258
 
204
259
  expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
205
260
  })
206
261
 
207
- it('correctly determines array equality 5', async () => {
262
+ it('correctly determines array equality 5', () => {
208
263
  const arrA = [{ key1: 'b' }, { key1: 'a' }, { key1: 'a' }];
209
264
  const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
210
265
 
211
- const result = await ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
212
- propA: {
213
- type: 'array',
214
- isElementEqual: (a, b) => a.key1 === b.key1
215
- }
216
- })
266
+ const parameterSettings = new ParsedResourceSettings({
267
+ id: 'type',
268
+ parameterSettings: {
269
+ propA: {
270
+ type: 'array',
271
+ isElementEqual: (a, b) => a.key1 === b.key1
272
+ }
273
+ },
274
+ }).parameterSettings
275
+
276
+ const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
217
277
 
218
278
  expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
219
279
  })
@@ -1,7 +1,6 @@
1
1
  import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
2
2
 
3
- import { ArrayParameterSetting, ParameterSetting, StatefulParameterSetting } from '../resource/resource-settings.js';
4
- import { areArraysEqual } from '../utils/utils.js';
3
+ import { ParameterSetting } from '../resource/resource-settings.js';
5
4
 
6
5
  /**
7
6
  * A parameter change describes a parameter level change to a resource.
@@ -85,12 +84,12 @@ export class ChangeSet<T extends StringIndexedObject> {
85
84
  return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
86
85
  }
87
86
 
88
- static async calculateModification<T extends StringIndexedObject>(
87
+ static calculateModification<T extends StringIndexedObject>(
89
88
  desired: Partial<T>,
90
89
  current: Partial<T>,
91
90
  parameterSettings: Partial<Record<keyof T, ParameterSetting>> = {},
92
- ): Promise<ChangeSet<T>> {
93
- const pc = await ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
91
+ ): ChangeSet<T> {
92
+ const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
94
93
 
95
94
  const statefulParameterKeys = new Set(
96
95
  Object.entries(parameterSettings)
@@ -126,11 +125,11 @@ export class ChangeSet<T extends StringIndexedObject> {
126
125
  * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
127
126
  * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
128
127
  */
129
- private static async calculateParameterChanges<T extends StringIndexedObject>(
128
+ private static calculateParameterChanges<T extends StringIndexedObject>(
130
129
  desiredParameters: Partial<T>,
131
130
  currentParameters: Partial<T>,
132
131
  parameterOptions?: Partial<Record<keyof T, ParameterSetting>>,
133
- ): Promise<ParameterChange<T>[]> {
132
+ ): ParameterChange<T>[] {
134
133
  const parameterChangeSet = new Array<ParameterChange<T>>();
135
134
 
136
135
  // Filter out null and undefined values or else the diff below will not work
@@ -155,7 +154,7 @@ export class ChangeSet<T extends StringIndexedObject> {
155
154
  continue;
156
155
  }
157
156
 
158
- if (!await ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
157
+ if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
159
158
  parameterChangeSet.push({
160
159
  name: k,
161
160
  previousValue: v ?? null,
@@ -210,26 +209,11 @@ export class ChangeSet<T extends StringIndexedObject> {
210
209
  return orderOfOperations[Math.max(indexPrev, indexNext)];
211
210
  }
212
211
 
213
- private static async isSame(
212
+ private static isSame(
214
213
  desired: unknown,
215
214
  current: unknown,
216
215
  setting?: ParameterSetting,
217
- ): Promise<boolean> {
218
- switch (setting?.type) {
219
- case 'stateful': {
220
- const statefulSetting = (setting as StatefulParameterSetting).definition.getSettings()
221
-
222
- return ChangeSet.isSame(desired, current, statefulSetting as ParameterSetting);
223
- }
224
-
225
- case 'array': {
226
- const arrayParameter = setting as ArrayParameterSetting;
227
- return areArraysEqual(arrayParameter, desired, current)
228
- }
229
-
230
- default: {
231
- return (setting?.isEqual ?? ((a, b) => a === b))(desired, current)
232
- }
233
- }
216
+ ): boolean {
217
+ return (setting?.isEqual ?? ((a, b) => a === b))(desired, current)
234
218
  }
235
219
  }
@@ -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, testPlan, TestResource } from '../utils/test-utils.test.js';
4
+ import { TestConfig, 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', async () => {
132
- const plan = await Plan.calculate<TestConfig>({
131
+ it('Returns the original resource names', () => {
132
+ const plan = Plan.calculate<TestConfig>({
133
133
  desiredParameters: { propA: 'propA' },
134
134
  currentParametersArray: [{ propA: 'propA2' }],
135
135
  stateParameters: null,
@@ -147,8 +147,7 @@ describe('Plan entity tests', () => {
147
147
  operation: ResourceOperation.RECREATE
148
148
  })
149
149
  })
150
- });
151
-
150
+ })
152
151
 
153
152
  function createTestResource() {
154
153
  return new class extends TestResource {
package/src/plan/plan.ts CHANGED
@@ -10,7 +10,6 @@ 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';
14
13
  import { ChangeSet } from './change-set.js';
15
14
 
16
15
  /**
@@ -106,14 +105,14 @@ export class Plan<T extends StringIndexedObject> {
106
105
  return this.coreParameters.type
107
106
  }
108
107
 
109
- static async calculate<T extends StringIndexedObject>(params: {
108
+ static calculate<T extends StringIndexedObject>(params: {
110
109
  desiredParameters: Partial<T> | null,
111
110
  currentParametersArray: Partial<T>[] | null,
112
111
  stateParameters: Partial<T> | null,
113
112
  coreParameters: ResourceConfig,
114
113
  settings: ParsedResourceSettings<T>,
115
114
  statefulMode: boolean,
116
- }): Promise<Plan<T>> {
115
+ }): Plan<T> {
117
116
  const {
118
117
  desiredParameters,
119
118
  currentParametersArray,
@@ -131,7 +130,7 @@ export class Plan<T extends StringIndexedObject> {
131
130
  statefulMode
132
131
  });
133
132
 
134
- const filteredCurrentParameters = await Plan.filterCurrentParams<T>({
133
+ const filteredCurrentParameters = Plan.filterCurrentParams<T>({
135
134
  desiredParameters,
136
135
  currentParameters,
137
136
  stateParameters,
@@ -167,7 +166,7 @@ export class Plan<T extends StringIndexedObject> {
167
166
  }
168
167
 
169
168
  // NO-OP, MODIFY or RE-CREATE
170
- const changeSet = await ChangeSet.calculateModification(
169
+ const changeSet = ChangeSet.calculateModification(
171
170
  desiredParameters!,
172
171
  filteredCurrentParameters!,
173
172
  settings.parameterSettings,
@@ -188,13 +187,13 @@ export class Plan<T extends StringIndexedObject> {
188
187
  * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
189
188
  * or wants to set. If a parameter is not specified then it's not managed by Codify.
190
189
  */
191
- private static async filterCurrentParams<T extends StringIndexedObject>(params: {
190
+ private static filterCurrentParams<T extends StringIndexedObject>(params: {
192
191
  desiredParameters: Partial<T> | null,
193
192
  currentParameters: Partial<T> | null,
194
193
  stateParameters: Partial<T> | null,
195
194
  settings: ResourceSettings<T>,
196
195
  statefulMode: boolean,
197
- }): Promise<Partial<T> | null> {
196
+ }): Partial<T> | null {
198
197
  const {
199
198
  desiredParameters: desired,
200
199
  currentParameters: current,
@@ -220,12 +219,10 @@ export class Plan<T extends StringIndexedObject> {
220
219
 
221
220
  // TODO: Add object handling here in addition to arrays in the future
222
221
  const arrayStatefulParameters = Object.fromEntries(
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
- );
222
+ Object.entries(filteredCurrent)
223
+ .filter(([k, v]) => isArrayStatefulParameter(k, v))
224
+ .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
225
+ )
229
226
 
230
227
  return { ...filteredCurrent, ...arrayStatefulParameters }
231
228
 
@@ -256,18 +253,31 @@ export class Plan<T extends StringIndexedObject> {
256
253
  && Array.isArray(v)
257
254
  }
258
255
 
259
- async function filterArrayStatefulParameter(k: string, v: unknown[]): Promise<unknown[]> {
256
+ // For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
257
+ function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
260
258
  const desiredArray = desired![k] as unknown[];
261
259
  const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
262
260
  .definition
263
261
  .getSettings() as ArrayParameterSetting)
264
- .isElementEqual;
262
+ .isElementEqual ?? ((a, b) => a === b);
265
263
 
266
- const eq = matcher ?? ((a: unknown, b: unknown) => a === b)
264
+ const desiredCopy = [...desiredArray];
265
+ const currentCopy = [...v];
266
+ const result = [];
267
267
 
268
- return asyncFilter(v, async (cv) =>
269
- asyncIncludes(desiredArray, async (dv) => eq(dv, cv))
270
- )
268
+ for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
269
+ const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2))
270
+
271
+ if (idx === -1) {
272
+ continue;
273
+ }
274
+
275
+ desiredCopy.splice(counter, 1)
276
+ const [element] = currentCopy.splice(idx, 1)
277
+ result.push(element)
278
+ }
279
+
280
+ return result;
271
281
  }
272
282
  }
273
283
 
@@ -1,14 +1,6 @@
1
1
  import { StringIndexedObject } from 'codify-schemas';
2
2
 
3
- import { areArraysEqual } from '../utils/utils.js';
4
- import {
5
- ArrayParameterSetting,
6
- ParameterEqualsDefaults,
7
- ParameterSetting,
8
- ParameterSettingType,
9
- ResourceSettings,
10
- StatefulParameterSetting
11
- } from './resource-settings.js';
3
+ import { ParameterSetting, resolveEqualsFn, ResourceSettings, StatefulParameterSetting } from './resource-settings.js';
12
4
  import { StatefulParameter as StatefulParameterImpl } from './stateful-parameter.js'
13
5
 
14
6
  export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
@@ -54,7 +46,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
54
46
  const settings = Object.entries(this.settings.parameterSettings ?? {})
55
47
  .map(([k, v]) => [k, v!] as const)
56
48
  .map(([k, v]) => {
57
- v.isEqual = this.resolveEqualsFn(v, k);
49
+ v.isEqual = resolveEqualsFn(v, k);
58
50
 
59
51
  return [k, v];
60
52
  })
@@ -154,18 +146,6 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
154
146
  // The rest of the types have defaults set already
155
147
  }
156
148
 
157
- private resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => Promise<boolean> | boolean {
158
- if (parameter.type === 'array') {
159
- return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
160
- }
161
-
162
- if (parameter.type === 'stateful') {
163
- return this.resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings(), key)
164
- }
165
-
166
- return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
167
- }
168
-
169
149
  private getFromCacheOrCreate<T2>(key: string, create: () => T2): T2 {
170
150
  if (this.cache.has(key)) {
171
151
  return this.cache.get(key) as T2
@@ -99,7 +99,7 @@ describe('Resource tests', () => {
99
99
  const resourceSpy = spy(resource);
100
100
 
101
101
  await controllerSpy.apply(
102
- await testPlan({
102
+ 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
- await testPlan({
119
+ 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
- await testPlan({
138
+ 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
- await testPlan<TestConfig>({
80
+ testPlan<TestConfig>({
81
81
  desired: { propA: 'a', propB: 0, propC: 'c' }
82
82
  })
83
83
  );
@@ -491,35 +491,4 @@ 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
- })
520
494
  })
521
-
522
- function sleep(ms: number) {
523
- return new Promise(resolve => setTimeout(resolve, ms));
524
- }
525
-
@@ -1,7 +1,7 @@
1
1
  import { StringIndexedObject } from 'codify-schemas';
2
2
  import path from 'node:path';
3
3
 
4
- import { untildify } from '../utils/utils.js';
4
+ import { areArraysEqual, untildify } from '../utils/utils.js';
5
5
  import { StatefulParameter } from './stateful-parameter.js';
6
6
 
7
7
  /**
@@ -127,7 +127,7 @@ export interface DefaultParameterSetting {
127
127
  *
128
128
  * @return Return true if equal
129
129
  */
130
- isEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
130
+ isEqual?: (desired: any, current: any) => 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) => Promise<boolean> | boolean;
159
+ isElementEqual?: (desired: any, current: any) => boolean
160
160
  }
161
161
 
162
162
  /**
@@ -183,10 +183,23 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
183
183
  order?: number,
184
184
  }
185
185
 
186
- export const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
186
+ const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
187
187
  'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
188
188
  'directory': (a: unknown, b: unknown) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
189
189
  'number': (a: unknown, b: unknown) => Number(a) === Number(b),
190
190
  'string': (a: unknown, b: unknown) => String(a) === String(b),
191
191
  'version': (desired: unknown, current: unknown) => String(current).includes(String(desired))
192
192
  }
193
+
194
+
195
+ export function resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean {
196
+ if (parameter.type === 'array') {
197
+ return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
198
+ }
199
+
200
+ if (parameter.type === 'stateful') {
201
+ return resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings(), key)
202
+ }
203
+
204
+ return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
205
+ }
@@ -1,13 +1,19 @@
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, TestResource } from '../utils/test-utils.test.js';
5
- import { ArrayParameterSetting, ResourceSettings } from './resource-settings.js';
4
+ import {
5
+ TestArrayStatefulParameter,
6
+ TestConfig,
7
+ testPlan,
8
+ TestResource,
9
+ TestStatefulParameter
10
+ } from '../utils/test-utils.test.js';
11
+ import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
6
12
  import { ResourceController } from './resource-controller.js';
7
13
 
8
14
  describe('Stateful parameter tests', () => {
9
15
  it('addItem is called the correct number of times', async () => {
10
- const plan = await testPlan<TestConfig>({
16
+ const plan = testPlan<TestConfig>({
11
17
  desired: { propZ: ['a', 'b', 'c'] },
12
18
  });
13
19
 
@@ -22,7 +28,7 @@ describe('Stateful parameter tests', () => {
22
28
  })
23
29
 
24
30
  it('applyRemoveItem is called the correct number of times', async () => {
25
- const plan = await testPlan<TestConfig>({
31
+ const plan = testPlan<TestConfig>({
26
32
  desired: null,
27
33
  current: [{ propZ: ['a', 'b', 'c'] }],
28
34
  state: { propZ: ['a', 'b', 'c'] },
@@ -41,7 +47,7 @@ describe('Stateful parameter tests', () => {
41
47
 
42
48
  it('In stateless mode only applyAddItem is called only for modifies', async () => {
43
49
  const parameter = new TestArrayStatefulParameter()
44
- const plan = await testPlan<TestConfig>({
50
+ const plan = testPlan<TestConfig>({
45
51
  desired: { propZ: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
46
52
  current: [{ propZ: ['a', 'b', 'c'] }],
47
53
  settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: parameter } } },
@@ -50,7 +56,7 @@ describe('Stateful parameter tests', () => {
50
56
  expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
51
57
  expect(plan.changeSet.parameterChanges[0]).toMatchObject({
52
58
  name: 'propZ',
53
- previousValue: ['a', 'c'], // In stateless mode the previous value gets filtered to prevent deletes
59
+ previousValue: ['c', 'a'], // In stateless mode the previous value gets filtered to prevent deletes
54
60
  newValue: ['a', 'c', 'd', 'e', 'f'],
55
61
  operation: ParameterOperation.MODIFY,
56
62
  })
@@ -72,7 +78,7 @@ describe('Stateful parameter tests', () => {
72
78
  }
73
79
  });
74
80
 
75
- const plan = await testPlan<TestConfig>({
81
+ const plan = testPlan<TestConfig>({
76
82
  desired: { propZ: ['9.12', '9.13'] }, // b to remove, d, e, f to add
77
83
  current: [{ propZ: ['9.12.9'] }],
78
84
  settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
@@ -92,31 +98,61 @@ describe('Stateful parameter tests', () => {
92
98
  expect(testParameter.removeItem.called).to.be.false;
93
99
  })
94
100
 
95
- it('isElementEqual works being async', async () => {
96
- const testParameter = spy(new class extends TestArrayStatefulParameter {
97
- getSettings(): ArrayParameterSetting {
101
+ it('isEqual works with type defaults', () => {
102
+ const testParameter = spy(new class extends TestStatefulParameter {
103
+ getSettings(): ParameterSetting {
98
104
  return {
99
- type: 'array',
100
- isElementEqual: async (desired, current) => {
101
- console.log(desired, current)
102
- await sleep(50);
103
- return true;
104
- }
105
+ type: 'version',
105
106
  }
106
107
  }
107
108
  });
108
109
 
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
110
+ const plan = testPlan<TestConfig>({
111
+ desired: { propZ: '20' },
112
+ current: [{ propZ: '20.17.0' }],
112
113
  settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
113
114
  });
114
115
 
115
- expect(plan.toResponse().operation).to.equal(ResourceOperation.NOOP);
116
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
116
117
  })
117
- })
118
118
 
119
- function sleep(ms: number) {
120
- return new Promise(resolve => setTimeout(resolve, ms));
121
- }
119
+ it('isElementEquals test', async () => {
120
+ const testParameter = spy(new class extends TestArrayStatefulParameter {
121
+ getSettings(): ArrayParameterSetting {
122
+ return {
123
+ type: 'array',
124
+ isElementEqual: (desired, current) => current.includes(desired),
125
+ }
126
+ }
127
+
128
+ async refresh(): Promise<any> {
129
+ return [
130
+ '20.15.0',
131
+ '20.15.1'
132
+ ]
133
+ }
134
+ });
135
+
136
+ const resource = new class extends TestResource {
137
+ getSettings(): ResourceSettings<any> {
138
+ return {
139
+ id: 'type',
140
+ parameterSettings: { nodeVersions: { type: 'stateful', definition: testParameter } }
141
+ }
142
+ }
122
143
 
144
+ async refresh(): Promise<Partial<any> | null> {
145
+ return {};
146
+ }
147
+ }
148
+
149
+ const controller = new ResourceController(resource);
150
+ const plan = await controller.plan({
151
+ nodeVersions: ['20.15'],
152
+ } as any)
153
+
154
+ console.log(JSON.stringify(plan, null, 2))
155
+
156
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
157
+ })
158
+ })
@@ -111,7 +111,7 @@ export function untildify(pathWithTilde: string) {
111
111
  return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
112
112
  }
113
113
 
114
- export async function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
114
+ export 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,12 +130,10 @@ 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
-
135
133
  // Algorithm for to check equality between two un-ordered; un-hashable arrays using
136
134
  // an isElementEqual method. Time: O(n^2)
137
135
  for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
138
- const idx = await asyncFindIndex(currentCopy, (e2) => eq(desiredCopy[counter], e2))
136
+ const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2))
139
137
 
140
138
  if (idx === -1) {
141
139
  return false;
@@ -147,56 +145,3 @@ Was provided even though type array was specified.
147
145
 
148
146
  return currentCopy.length === 0;
149
147
  }
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
- }