codify-plugin-lib 1.0.41 → 1.0.43

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.
@@ -12,13 +12,12 @@ export declare class ChangeSet<T extends StringIndexedObject> {
12
12
  constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
13
13
  get desiredParameters(): T;
14
14
  get currentParameters(): T;
15
- static newCreate<T extends {}>(desiredConfig: T): ChangeSet<StringIndexedObject>;
16
15
  static calculateParameterChangeSet<T extends StringIndexedObject>(desired: T | null, current: T | null, options: {
17
16
  statefulMode: boolean;
18
17
  parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
19
18
  }): ParameterChange<T>[];
20
19
  static combineResourceOperations(prev: ResourceOperation, next: ResourceOperation): ResourceOperation;
21
- static isSame(a: unknown, b: unknown, isEqual?: (a: unknown, b: unknown) => boolean): boolean;
20
+ static isSame(desired: unknown, current: unknown, configuration?: ParameterConfiguration): boolean;
22
21
  private static calculateStatefulModeChangeSet;
23
22
  private static calculateStatelessModeChangeSet;
24
23
  }
@@ -20,19 +20,6 @@ export class ChangeSet {
20
20
  [pc.name]: pc.previousValue,
21
21
  }), {});
22
22
  }
23
- static newCreate(desiredConfig) {
24
- const parameterChangeSet = Object.entries(desiredConfig)
25
- .filter(([k,]) => k !== 'type' && k !== 'name')
26
- .map(([k, v]) => {
27
- return {
28
- name: k,
29
- operation: ParameterOperation.ADD,
30
- previousValue: null,
31
- newValue: v,
32
- };
33
- });
34
- return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
35
- }
36
23
  static calculateParameterChangeSet(desired, current, options) {
37
24
  if (options.statefulMode) {
38
25
  return ChangeSet.calculateStatefulModeChangeSet(desired, current, options.parameterConfigurations);
@@ -53,16 +40,22 @@ export class ChangeSet {
53
40
  const indexNext = orderOfOperations.indexOf(next);
54
41
  return orderOfOperations[Math.max(indexPrev, indexNext)];
55
42
  }
56
- static isSame(a, b, isEqual) {
57
- if (isEqual) {
58
- return isEqual(a, b);
43
+ static isSame(desired, current, configuration) {
44
+ if (configuration?.isEqual) {
45
+ return configuration.isEqual(desired, current);
59
46
  }
60
- if (Array.isArray(a) && Array.isArray(b)) {
61
- const sortedPrev = a.map((x) => x).sort();
62
- const sortedNext = b.map((x) => x).sort();
63
- return JSON.stringify(sortedPrev) === JSON.stringify(sortedNext);
47
+ if (Array.isArray(desired) && Array.isArray(current)) {
48
+ const sortedDesired = desired.map((x) => x).sort();
49
+ const sortedCurrent = current.map((x) => x).sort();
50
+ if (sortedDesired.length !== sortedCurrent.length) {
51
+ return false;
52
+ }
53
+ if (configuration?.isElementEqual) {
54
+ return sortedDesired.every((value, index) => configuration.isElementEqual(value, sortedCurrent[index]));
55
+ }
56
+ return JSON.stringify(sortedDesired) === JSON.stringify(sortedCurrent);
64
57
  }
65
- return a === b;
58
+ return desired === current;
66
59
  }
67
60
  static calculateStatefulModeChangeSet(desired, current, parameterConfigurations) {
68
61
  const parameterChangeSet = new Array();
@@ -79,7 +72,7 @@ export class ChangeSet {
79
72
  delete _current[k];
80
73
  continue;
81
74
  }
82
- if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]?.isEqual)) {
75
+ if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
83
76
  parameterChangeSet.push({
84
77
  name: k,
85
78
  previousValue: v,
@@ -126,7 +119,7 @@ export class ChangeSet {
126
119
  });
127
120
  continue;
128
121
  }
129
- if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]?.isEqual)) {
122
+ if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
130
123
  parameterChangeSet.push({
131
124
  name: k,
132
125
  previousValue: _current[k],
@@ -2,6 +2,7 @@ import { ResourceOperation } from 'codify-schemas';
2
2
  export interface ParameterConfiguration {
3
3
  planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
4
4
  isEqual?: (desired: any, current: any) => boolean;
5
+ isElementEqual?: (desired: any, current: any) => boolean;
5
6
  isStatefulParameter?: boolean;
6
7
  }
7
8
  export interface PlanConfiguration<T> {
@@ -6,7 +6,7 @@ export declare class Plan<T extends StringIndexedObject> {
6
6
  changeSet: ChangeSet<T>;
7
7
  resourceMetadata: ResourceConfig;
8
8
  constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
9
- static create<T extends StringIndexedObject>(desiredConfig: Partial<T> & ResourceConfig, currentConfig: Partial<T> & ResourceConfig | null, configuration: PlanConfiguration<T>): Plan<T>;
9
+ static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, configuration: PlanConfiguration<T>): Plan<T>;
10
10
  getResourceType(): string;
11
11
  static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
12
12
  get desiredConfig(): T;
@@ -1,7 +1,6 @@
1
1
  import { ChangeSet } from './change-set.js';
2
2
  import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
3
3
  import { randomUUID } from 'crypto';
4
- import { splitUserConfig } from '../utils/utils.js';
5
4
  export class Plan {
6
5
  id;
7
6
  changeSet;
@@ -11,19 +10,17 @@ export class Plan {
11
10
  this.changeSet = changeSet;
12
11
  this.resourceMetadata = resourceMetadata;
13
12
  }
14
- static create(desiredConfig, currentConfig, configuration) {
13
+ static create(desiredParameters, currentParameters, resourceMetadata, configuration) {
15
14
  const parameterConfigurations = configuration.parameterConfigurations ?? {};
16
15
  const statefulParameterNames = new Set([...Object.entries(parameterConfigurations)]
17
16
  .filter(([k, v]) => v.isStatefulParameter)
18
17
  .map(([k, v]) => k));
19
- const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
20
- const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
21
18
  const parameterChangeSet = ChangeSet.calculateParameterChangeSet(desiredParameters, currentParameters, { statefulMode: configuration.statefulMode, parameterConfigurations });
22
19
  let resourceOperation;
23
- if (!currentConfig && desiredConfig) {
20
+ if (!currentParameters && desiredParameters) {
24
21
  resourceOperation = ResourceOperation.CREATE;
25
22
  }
26
- else if (currentConfig && !desiredConfig) {
23
+ else if (currentParameters && !desiredParameters) {
27
24
  resourceOperation = ResourceOperation.DESTROY;
28
25
  }
29
26
  else {
@@ -33,18 +33,18 @@ export class Resource {
33
33
  const keysToRefresh = new Set(Object.keys(resourceParameters));
34
34
  const currentParameters = await this.refresh(keysToRefresh);
35
35
  if (currentParameters == null) {
36
- return Plan.create(desiredConfig, null, planConfiguration);
36
+ return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
37
37
  }
38
38
  this.validateRefreshResults(currentParameters, keysToRefresh);
39
39
  for (const statefulParameter of statefulParameters) {
40
40
  const desiredValue = desiredParameters[statefulParameter.name];
41
- let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
41
+ let currentValue = await statefulParameter.refresh() ?? undefined;
42
42
  if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
43
43
  currentValue = currentValue.filter((p) => desiredValue?.includes(p));
44
44
  }
45
45
  currentParameters[statefulParameter.name] = currentValue;
46
46
  }
47
- return Plan.create(desiredConfig, { ...currentParameters, ...resourceMetadata }, planConfiguration);
47
+ return Plan.create(desiredParameters, currentParameters, resourceMetadata, planConfiguration);
48
48
  }
49
49
  async apply(plan) {
50
50
  if (plan.getResourceType() !== this.typeId) {
@@ -2,23 +2,28 @@ import { Plan } from './plan.js';
2
2
  import { StringIndexedObject } from 'codify-schemas';
3
3
  export interface StatefulParameterConfiguration<T> {
4
4
  name: keyof T;
5
- isEqual?: (a: any, b: any) => boolean;
5
+ isEqual?: (desired: any, current: any) => boolean;
6
+ }
7
+ export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {
8
+ isEqual?: (desired: any[], current: any[]) => boolean;
9
+ isElementEqual?: (desired: any, current: any) => boolean;
6
10
  }
7
11
  export declare abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
8
12
  readonly name: keyof T;
9
13
  readonly configuration: StatefulParameterConfiguration<T>;
10
14
  protected constructor(configuration: StatefulParameterConfiguration<T>);
11
- abstract refresh(previousValue: V | null): Promise<V | null>;
15
+ abstract refresh(): Promise<V | null>;
12
16
  abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
13
17
  abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
14
18
  abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
15
19
  }
16
20
  export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
17
- protected constructor(configuration: StatefulParameterConfiguration<T>);
21
+ configuration: ArrayStatefulParameterConfiguration<T>;
22
+ constructor(configuration: ArrayStatefulParameterConfiguration<T>);
18
23
  applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
19
24
  applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
20
25
  applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
21
- abstract refresh(previousValue: V[] | null): Promise<V[] | null>;
26
+ abstract refresh(): Promise<V[] | null>;
22
27
  abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
23
28
  abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
24
29
  }
@@ -7,8 +7,10 @@ export class StatefulParameter {
7
7
  }
8
8
  }
9
9
  export class ArrayStatefulParameter extends StatefulParameter {
10
+ configuration;
10
11
  constructor(configuration) {
11
12
  super(configuration);
13
+ this.configuration = configuration;
12
14
  }
13
15
  async applyAdd(valuesToAdd, plan) {
14
16
  for (const value of valuesToAdd) {
@@ -16,8 +18,19 @@ export class ArrayStatefulParameter extends StatefulParameter {
16
18
  }
17
19
  }
18
20
  async applyModify(newValues, previousValues, allowDeletes, plan) {
19
- const valuesToAdd = newValues.filter((n) => !previousValues.includes(n));
20
- const valuesToRemove = previousValues.filter((n) => !newValues.includes(n));
21
+ const configuration = this.configuration;
22
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
23
+ if ((configuration).isElementEqual) {
24
+ return configuration.isElementEqual(n, p);
25
+ }
26
+ return n === p;
27
+ }));
28
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
29
+ if ((configuration).isElementEqual) {
30
+ return configuration.isElementEqual(n, p);
31
+ }
32
+ return n === p;
33
+ }));
21
34
  for (const value of valuesToAdd) {
22
35
  await this.applyAddItem(value, plan);
23
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -43,20 +43,20 @@ export class ChangeSet<T extends StringIndexedObject> {
43
43
  // const operation = ChangeSet.combineResourceOperations(prev, );
44
44
  // }
45
45
 
46
- static newCreate<T extends {}>(desiredConfig: T) {
47
- const parameterChangeSet = Object.entries(desiredConfig)
48
- .filter(([k,]) => k !== 'type' && k !== 'name')
49
- .map(([k, v]) => {
50
- return {
51
- name: k,
52
- operation: ParameterOperation.ADD,
53
- previousValue: null,
54
- newValue: v,
55
- }
56
- })
57
-
58
- return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
59
- }
46
+ // static newCreate<T extends {}>(desiredConfig: T) {
47
+ // const parameterChangeSet = Object.entries(desiredConfig)
48
+ // .filter(([k,]) => k !== 'type' && k !== 'name')
49
+ // .map(([k, v]) => {
50
+ // return {
51
+ // name: k,
52
+ // operation: ParameterOperation.ADD,
53
+ // previousValue: null,
54
+ // newValue: v,
55
+ // }
56
+ // })
57
+ //
58
+ // return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
59
+ // }
60
60
 
61
61
  static calculateParameterChangeSet<T extends StringIndexedObject>(
62
62
  desired: T | null,
@@ -86,22 +86,32 @@ export class ChangeSet<T extends StringIndexedObject> {
86
86
  }
87
87
 
88
88
  static isSame(
89
- a: unknown,
90
- b: unknown,
91
- isEqual?: (a: unknown, b: unknown) => boolean,
89
+ desired: unknown,
90
+ current: unknown,
91
+ configuration?: ParameterConfiguration,
92
92
  ): boolean {
93
- if (isEqual) {
94
- return isEqual(a, b);
93
+ if (configuration?.isEqual) {
94
+ return configuration.isEqual(desired, current);
95
95
  }
96
96
 
97
- if (Array.isArray(a) && Array.isArray(b)) {
98
- const sortedPrev = a.map((x) => x).sort();
99
- const sortedNext = b.map((x) => x).sort();
97
+ if (Array.isArray(desired) && Array.isArray(current)) {
98
+ const sortedDesired = desired.map((x) => x).sort();
99
+ const sortedCurrent = current.map((x) => x).sort();
100
+
101
+ if (sortedDesired.length !== sortedCurrent.length) {
102
+ return false;
103
+ }
104
+
105
+ if (configuration?.isElementEqual) {
106
+ return sortedDesired.every((value, index) =>
107
+ configuration.isElementEqual!(value, sortedCurrent[index])
108
+ );
109
+ }
100
110
 
101
- return JSON.stringify(sortedPrev) === JSON.stringify(sortedNext);
111
+ return JSON.stringify(sortedDesired) === JSON.stringify(sortedCurrent);
102
112
  }
103
113
 
104
- return a === b;
114
+ return desired === current;
105
115
  }
106
116
 
107
117
  // Explanation: Stateful mode means that codify maintains a stateful to keep track of resources it has added.
@@ -130,7 +140,7 @@ export class ChangeSet<T extends StringIndexedObject> {
130
140
  continue;
131
141
  }
132
142
 
133
- if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]?.isEqual)) {
143
+ if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
134
144
  parameterChangeSet.push({
135
145
  name: k,
136
146
  previousValue: v,
@@ -194,7 +204,7 @@ export class ChangeSet<T extends StringIndexedObject> {
194
204
  continue;
195
205
  }
196
206
 
197
- if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k]?.isEqual)) {
207
+ if (!ChangeSet.isSame(_desired[k], _current[k], parameterConfigurations?.[k])) {
198
208
  parameterChangeSet.push({
199
209
  name: k,
200
210
  previousValue: _current[k],
@@ -15,6 +15,8 @@ export interface ParameterConfiguration {
15
15
  */
16
16
  isEqual?: (desired: any, current: any) => boolean;
17
17
 
18
+ isElementEqual?: (desired: any, current: any) => boolean;
19
+
18
20
  isStatefulParameter?: boolean;
19
21
  }
20
22
 
@@ -9,7 +9,6 @@ import {
9
9
  } from 'codify-schemas';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
12
- import { splitUserConfig } from '../utils/utils.js';
13
12
 
14
13
  export class Plan<T extends StringIndexedObject> {
15
14
  id: string;
@@ -23,8 +22,9 @@ export class Plan<T extends StringIndexedObject> {
23
22
  }
24
23
 
25
24
  static create<T extends StringIndexedObject>(
26
- desiredConfig: Partial<T> & ResourceConfig,
27
- currentConfig: Partial<T> & ResourceConfig | null,
25
+ desiredParameters: Partial<T> | null,
26
+ currentParameters: Partial<T> | null,
27
+ resourceMetadata: ResourceConfig,
28
28
  configuration: PlanConfiguration<T>
29
29
  ): Plan<T> {
30
30
  const parameterConfigurations = configuration.parameterConfigurations ?? {} as Record<keyof T, ParameterConfiguration>;
@@ -34,9 +34,6 @@ export class Plan<T extends StringIndexedObject> {
34
34
  .map(([k, v]) => k)
35
35
  );
36
36
 
37
- const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
38
- const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
39
-
40
37
 
41
38
  // TODO: After adding in state files, need to calculate deletes here
42
39
  // Where current config exists and state config exists but desired config doesn't
@@ -51,9 +48,9 @@ export class Plan<T extends StringIndexedObject> {
51
48
  );
52
49
 
53
50
  let resourceOperation: ResourceOperation;
54
- if (!currentConfig && desiredConfig) {
51
+ if (!currentParameters && desiredParameters) {
55
52
  resourceOperation = ResourceOperation.CREATE;
56
- } else if (currentConfig && !desiredConfig) {
53
+ } else if (currentParameters && !desiredParameters) {
57
54
  resourceOperation = ResourceOperation.DESTROY;
58
55
  } else {
59
56
  resourceOperation = parameterChangeSet
@@ -50,8 +50,9 @@ describe('Resource parameters tests', () => {
50
50
  const resourceSpy = spy(resource);
51
51
  const result = await resourceSpy.apply(
52
52
  Plan.create<TestConfig>(
53
- { type: 'resource', propA: 'a', propB: 0, propC: 'b' },
53
+ { propA: 'a', propB: 0, propC: 'b' },
54
54
  null,
55
+ { type: 'resource' },
55
56
  { statefulMode: false },
56
57
  )
57
58
  );
@@ -136,6 +136,7 @@ describe('Resource tests', () => {
136
136
  const result = await resourceSpy.apply(
137
137
  Plan.create<TestConfig>(
138
138
  { type: 'resource', propA: 'a', propB: 0 },
139
+ null,
139
140
  { type: 'resource' },
140
141
  { statefulMode: false },
141
142
  )
@@ -154,8 +155,9 @@ describe('Resource tests', () => {
154
155
  const resourceSpy = spy(resource);
155
156
  const result = await resourceSpy.apply(
156
157
  Plan.create<TestConfig>(
158
+ null,
159
+ { propA: 'a', propB: 0 },
157
160
  { type: 'resource' },
158
- { type: 'resource', propA: 'a', propB: 0 },
159
161
  { statefulMode: true },
160
162
  )
161
163
  )
@@ -173,8 +175,9 @@ describe('Resource tests', () => {
173
175
  const resourceSpy = spy(resource);
174
176
  const result = await resourceSpy.apply(
175
177
  Plan.create<TestConfig>(
176
- { type: 'resource', propA: 'a', propB: 0 },
177
- { type: 'resource', propA: 'b', propB: -1 },
178
+ { propA: 'a', propB: 0 },
179
+ { propA: 'b', propB: -1 },
180
+ { type: 'resource' },
178
181
  { statefulMode: true },
179
182
  )
180
183
  );
@@ -66,7 +66,7 @@ export abstract class Resource<T extends StringIndexedObject> {
66
66
 
67
67
  // Short circuit here. If resource is non-existent, then there's no point checking stateful parameters
68
68
  if (currentParameters == null) {
69
- return Plan.create(desiredConfig, null, planConfiguration);
69
+ return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
70
70
  }
71
71
 
72
72
  this.validateRefreshResults(currentParameters, keysToRefresh);
@@ -76,7 +76,7 @@ export abstract class Resource<T extends StringIndexedObject> {
76
76
  for(const statefulParameter of statefulParameters) {
77
77
  const desiredValue = desiredParameters[statefulParameter.name];
78
78
 
79
- let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
79
+ let currentValue = await statefulParameter.refresh() ?? undefined;
80
80
 
81
81
  // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
82
82
  if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
@@ -87,8 +87,9 @@ export abstract class Resource<T extends StringIndexedObject> {
87
87
  }
88
88
 
89
89
  return Plan.create(
90
- desiredConfig,
91
- { ...currentParameters, ...resourceMetadata } as Partial<T> & ResourceConfig,
90
+ desiredParameters,
91
+ currentParameters as Partial<T>,
92
+ resourceMetadata,
92
93
  planConfiguration,
93
94
  )
94
95
  }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ArrayStatefulParameter, ArrayStatefulParameterConfiguration, } from './stateful-parameter.js';
3
+ import { Plan } from './plan.js';
4
+ import { spy } from 'sinon';
5
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
6
+
7
+ interface TestConfig {
8
+ propA: string[];
9
+ [x: string]: unknown;
10
+ }
11
+
12
+ class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
13
+ constructor(configuration?: ArrayStatefulParameterConfiguration<TestConfig>) {
14
+ super(configuration ?? {
15
+ name: 'propA'
16
+ })
17
+ }
18
+
19
+ async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
20
+ async applyRemoveItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
21
+
22
+ async refresh(): Promise<string[] | null> {
23
+ return null;
24
+ }
25
+ }
26
+
27
+
28
+ describe('Stateful parameter tests', () => {
29
+ it('applyAddItem is called the correct number of times', async () => {
30
+ const plan = Plan.create<TestConfig>(
31
+ { propA: ['a', 'b', 'c'] },
32
+ null,
33
+ { type: 'typeA' },
34
+ { statefulMode: false }
35
+ );
36
+
37
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
38
+ expect(plan.changeSet.parameterChanges.length).to.eq(1);
39
+
40
+ const testParameter = spy(new TestArrayParameter());
41
+ await testParameter.applyAdd(plan.desiredConfig.propA, plan);
42
+
43
+ expect(testParameter.applyAddItem.callCount).to.eq(3);
44
+ expect(testParameter.applyRemoveItem.called).to.be.false;
45
+ })
46
+
47
+ it('applyRemoveItem is called the correct number of times', async () => {
48
+ const plan = Plan.create<TestConfig>(
49
+ null,
50
+ { propA: ['a', 'b', 'c'] },
51
+ { type: 'typeA' },
52
+ { statefulMode: true }
53
+ );
54
+
55
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
56
+ expect(plan.changeSet.parameterChanges.length).to.eq(1);
57
+
58
+ const testParameter = spy(new TestArrayParameter());
59
+ await testParameter.applyRemove(plan.currentConfig.propA, plan);
60
+
61
+ expect(testParameter.applyAddItem.called).to.be.false;
62
+ expect(testParameter.applyRemoveItem.callCount).to.eq(3);
63
+ })
64
+
65
+ it('In stateless mode only applyAddItem is called only for modifies', async () => {
66
+ const plan = Plan.create<TestConfig>(
67
+ { propA: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
68
+ { propA: ['a', 'b', 'c'] },
69
+ { type: 'typeA' },
70
+ { statefulMode: true, parameterConfigurations: { propA: { isStatefulParameter: true }} }
71
+ );
72
+
73
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
74
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
75
+ name: 'propA',
76
+ previousValue: ['a', 'b', 'c'],
77
+ newValue: ['a', 'c', 'd', 'e', 'f'],
78
+ operation: ParameterOperation.MODIFY,
79
+ })
80
+
81
+ const testParameter = spy(new TestArrayParameter());
82
+ await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
83
+
84
+ expect(testParameter.applyAddItem.calledThrice).to.be.true;
85
+ expect(testParameter.applyRemoveItem.called).to.be.false;
86
+ })
87
+
88
+ it('isElementEqual is called for modifies', async () => {
89
+ const plan = Plan.create<TestConfig>(
90
+ { propA: ['9.12', '9.13'] }, // b to remove, d, e, f to add
91
+ { propA: ['9.12.9'] },
92
+ { type: 'typeA' },
93
+ { statefulMode: false, parameterConfigurations: { propA: { isStatefulParameter: true }} }
94
+ );
95
+
96
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
97
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
98
+ name: 'propA',
99
+ previousValue: ['9.12.9'],
100
+ newValue: ['9.12', '9.13'],
101
+ operation: ParameterOperation.MODIFY,
102
+ })
103
+
104
+ const testParameter = spy(new class extends TestArrayParameter {
105
+ constructor() {
106
+ super({
107
+ name: 'propA',
108
+ isElementEqual: (desired, current) => current.includes(desired),
109
+ });
110
+ }
111
+ });
112
+
113
+ await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
114
+
115
+ expect(testParameter.applyAddItem.calledOnce).to.be.true;
116
+ expect(testParameter.applyRemoveItem.called).to.be.false;
117
+ })
118
+ })
@@ -3,9 +3,15 @@ import { StringIndexedObject } from 'codify-schemas';
3
3
 
4
4
  export interface StatefulParameterConfiguration<T> {
5
5
  name: keyof T;
6
- isEqual?: (a: any, b: any) => boolean;
6
+ isEqual?: (desired: any, current: any) => boolean;
7
7
  }
8
8
 
9
+ export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {
10
+ isEqual?: (desired: any[], current: any[]) => boolean;
11
+ isElementEqual?: (desired: any, current: any) => boolean;
12
+ }
13
+
14
+
9
15
  export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
10
16
  readonly name: keyof T;
11
17
  readonly configuration: StatefulParameterConfiguration<T>;
@@ -15,7 +21,7 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
15
21
  this.configuration = configuration
16
22
  }
17
23
 
18
- abstract refresh(previousValue: V | null): Promise<V | null>;
24
+ abstract refresh(): Promise<V | null>;
19
25
 
20
26
  // TODO: Add an additional parameter here for what has actually changed.
21
27
  abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
@@ -24,8 +30,11 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
24
30
  }
25
31
 
26
32
  export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
27
- protected constructor(configuration: StatefulParameterConfiguration<T>) {
33
+ configuration: ArrayStatefulParameterConfiguration<T>;
34
+
35
+ constructor(configuration: ArrayStatefulParameterConfiguration<T>) {
28
36
  super(configuration);
37
+ this.configuration = configuration;
29
38
  }
30
39
 
31
40
  async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
@@ -35,8 +44,21 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
35
44
  }
36
45
 
37
46
  async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
38
- const valuesToAdd = newValues.filter((n) => !previousValues.includes(n));
39
- const valuesToRemove = previousValues.filter((n) => !newValues.includes(n));
47
+ const configuration = this.configuration as ArrayStatefulParameterConfiguration<T>;
48
+
49
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
50
+ if ((configuration).isElementEqual) {
51
+ return configuration.isElementEqual(n, p);
52
+ }
53
+ return n === p;
54
+ }));
55
+
56
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
57
+ if ((configuration).isElementEqual) {
58
+ return configuration.isElementEqual(n, p);
59
+ }
60
+ return n === p;
61
+ }));
40
62
 
41
63
  for (const value of valuesToAdd) {
42
64
  await this.applyAddItem(value, plan)
@@ -55,7 +77,7 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
55
77
  }
56
78
  }
57
79
 
58
- abstract refresh(previousValue: V[] | null): Promise<V[] | null>;
80
+ abstract refresh(): Promise<V[] | null>;
59
81
  abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
60
82
  abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
61
83
  }