codify-plugin-lib 1.0.50 → 1.0.51

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.
@@ -2,10 +2,13 @@ import { ParameterOperation, ResourceConfig, ResourceOperation, StringIndexedObj
2
2
  import { ParameterChange } from './change-set.js';
3
3
  import { Plan } from './plan.js';
4
4
  import { StatefulParameter } from './stateful-parameter.js';
5
- import { ResourceConfiguration, ValidationResult } from './resource-types.js';
5
+ import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
6
6
  import { setsEqual, splitUserConfig } from '../utils/utils.js';
7
- import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
7
+ import { ParameterOptions, PlanOptions } from './plan-types.js';
8
8
  import { TransformParameter } from './transform-parameter.js';
9
+ import { ResourceOptions, ResourceOptionsParser } from './resource-options.js';
10
+ import Ajv from 'ajv';
11
+ import Ajv2020, { ValidateFunction } from 'ajv/dist/2020.js';
9
12
 
10
13
  /**
11
14
  * Description of resource here
@@ -15,95 +18,99 @@ import { TransformParameter } from './transform-parameter.js';
15
18
  *
16
19
  */
17
20
  export abstract class Resource<T extends StringIndexedObject> {
18
-
19
21
  readonly typeId: string;
20
22
  readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
21
23
  readonly transformParameters: Map<keyof T, TransformParameter<T>>
24
+ readonly resourceParameters: Map<keyof T, ResourceParameterOptions>;
25
+
26
+ readonly statefulParameterOrder: Map<keyof T, number>;
27
+ readonly transformParameterOrder: Map<keyof T, number>;
28
+
22
29
  readonly dependencies: string[]; // TODO: Change this to a string
23
- readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>
24
- readonly configuration: ResourceConfiguration<T>;
30
+ readonly parameterOptions: Record<keyof T, ParameterOptions>
31
+ readonly options: ResourceOptions<T>;
25
32
  readonly defaultValues: Partial<Record<keyof T, unknown>>;
26
33
 
27
- protected constructor(configuration: ResourceConfiguration<T>) {
28
- this.validateResourceConfiguration(configuration);
34
+ protected ajv?: Ajv.default;
35
+ protected schemaValidator?: ValidateFunction;
29
36
 
30
- this.typeId = configuration.type;
31
- this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
32
- this.transformParameters = new Map(Object.entries(configuration.transformParameters ?? {})) as Map<keyof T, TransformParameter<T>>;
33
- this.parameterConfigurations = this.initializeParameterConfigurations(configuration);
34
- this.defaultValues = this.initializeDefaultValues(configuration);
37
+ protected constructor(options: ResourceOptions<T>) {
38
+ this.typeId = options.type;
39
+ this.dependencies = options.dependencies ?? [];
40
+ this.options = options;
35
41
 
36
- this.dependencies = configuration.dependencies ?? [];
37
- this.configuration = configuration;
42
+ if (this.options.schema) {
43
+ this.ajv = new Ajv2020.default({
44
+ strict: true,
45
+ strictRequired: false,
46
+ })
47
+ this.schemaValidator = this.ajv.compile(this.options.schema);
48
+ }
49
+
50
+ const parser = new ResourceOptionsParser<T>(options);
51
+ this.statefulParameters = parser.statefulParameters;
52
+ this.transformParameters = parser.transformParameters;
53
+ this.resourceParameters = parser.resourceParameters;
54
+ this.parameterOptions = parser.changeSetParameterOptions;
55
+ this.defaultValues = parser.defaultValues;
56
+ this.statefulParameterOrder = parser.statefulParameterOrder;
57
+ this.transformParameterOrder = parser.transformParameterOrder;
38
58
  }
39
59
 
40
60
  async onInitialize(): Promise<void> {}
41
61
 
62
+ async validateResource(parameters: unknown): Promise<ValidationResult> {
63
+ if (this.schemaValidator) {
64
+ const isValid = this.schemaValidator(parameters);
65
+
66
+ if (!isValid) {
67
+ return {
68
+ isValid: false,
69
+ errors: this.schemaValidator?.errors ?? [],
70
+ }
71
+ }
72
+ }
73
+
74
+ return this.validate(parameters);
75
+ }
76
+
42
77
  // TODO: Add state in later.
43
78
  // Currently only calculating how to add things to reach desired state. Can't delete resources.
44
79
  // Add previousConfig as a parameter for plan(desired, previous);
45
80
  async plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>> {
46
-
47
- // Explanation: these are settings for how the plan will be generated
48
- const planConfiguration: PlanConfiguration<T> = {
81
+ const planOptions: PlanOptions<T> = {
49
82
  statefulMode: false,
50
- parameterConfigurations: this.parameterConfigurations,
83
+ parameterOptions: this.parameterOptions,
51
84
  }
52
85
 
53
- const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
54
-
55
- this.addDefaultValues(desiredParameters);
56
- await this.applyTransformParameters(desiredParameters);
57
-
58
- const resourceParameters = Object.fromEntries([
59
- ...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
60
- ]) as Partial<T>;
86
+ // Parse data from the user supplied config
87
+ const parsedConfig = new ConfigParser(desiredConfig, this.statefulParameters, this.transformParameters)
88
+ const {
89
+ parameters: desiredParameters,
90
+ resourceMetadata,
91
+ resourceParameters,
92
+ statefulParameters
93
+ } = parsedConfig;
61
94
 
62
- const statefulParameters = [...this.statefulParameters.values()]
63
- .filter((sp) => desiredParameters[sp.name] !== undefined) // Checking for undefined is fine here because JSONs can only have null.
95
+ this.addDefaultValues(resourceParameters);
96
+ await this.applyTransformParameters(resourceParameters);
64
97
 
65
- // Refresh resource parameters
66
- // This refreshes the parameters that configure the resource itself
67
- const entriesToRefresh = new Map(Object.entries(resourceParameters));
68
- const currentParameters = await this.refresh(entriesToRefresh);
98
+ // Refresh resource parameters. This refreshes the parameters that configure the resource itself
99
+ const currentParameters = await this.refreshResourceParameters(resourceParameters);
69
100
 
70
- // Short circuit here. If resource is non-existent, then there's no point checking stateful parameters
101
+ // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
71
102
  if (currentParameters == null) {
72
- return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
103
+ return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
73
104
  }
74
105
 
75
- this.validateRefreshResults(currentParameters, entriesToRefresh);
76
-
77
- // Refresh stateful parameters
78
- // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
79
- for(const statefulParameter of statefulParameters) {
80
- const desiredValue = desiredParameters[statefulParameter.name];
81
-
82
- let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
83
-
84
- // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
85
- if (Array.isArray(currentValue)
86
- && Array.isArray(desiredValue)
87
- && !planConfiguration.statefulMode
88
- && !statefulParameter.configuration.disableStatelessModeArrayFiltering
89
- ) {
90
- currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
91
- const pc = planConfiguration?.parameterConfigurations?.[statefulParameter.name];
92
- if (pc && pc.isElementEqual) {
93
- return pc.isElementEqual(d, c);
94
- }
95
- return d === c;
96
- })) as any;
97
- }
98
-
99
- currentParameters[statefulParameter.name] = currentValue;
100
- }
106
+ // Refresh stateful parameters. These parameters have state external to the resource
107
+ const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
101
108
 
102
109
  return Plan.create(
103
- desiredParameters,
104
- currentParameters as Partial<T>,
110
+ { ...resourceParameters, ...statefulParameters },
111
+ { ...currentParameters, ...statefulCurrentParameters } as Partial<T>,
105
112
  resourceMetadata,
106
- planConfiguration,
113
+ planOptions,
107
114
  )
108
115
  }
109
116
 
@@ -134,6 +141,8 @@ export abstract class Resource<T extends StringIndexedObject> {
134
141
 
135
142
  const statefulParameterChanges = plan.changeSet.parameterChanges
136
143
  .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
144
+ .sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
145
+
137
146
  for (const parameterChange of statefulParameterChanges) {
138
147
  const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
139
148
  await statefulParameter.applyAdd(parameterChange.newValue, plan);
@@ -148,6 +157,7 @@ export abstract class Resource<T extends StringIndexedObject> {
148
157
 
149
158
  const statelessParameterChanges = parameterChanges
150
159
  .filter((pc: ParameterChange<T>) => !this.statefulParameters.has(pc.name))
160
+
151
161
  for (const pc of statelessParameterChanges) {
152
162
  // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
153
163
  await this.applyModify(pc.name, pc.newValue, pc.previousValue, false, plan);
@@ -155,6 +165,8 @@ export abstract class Resource<T extends StringIndexedObject> {
155
165
 
156
166
  const statefulParameterChanges = parameterChanges
157
167
  .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
168
+ .sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
169
+
158
170
  for (const parameterChange of statefulParameterChanges) {
159
171
  const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
160
172
 
@@ -179,9 +191,11 @@ export abstract class Resource<T extends StringIndexedObject> {
179
191
  private async _applyDestroy(plan: Plan<T>): Promise<void> {
180
192
  // If this option is set (defaults to false), then stateful parameters need to be destroyed
181
193
  // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
182
- if (this.configuration.callStatefulParameterRemoveOnDestroy) {
194
+ if (this.options.callStatefulParameterRemoveOnDestroy) {
183
195
  const statefulParameterChanges = plan.changeSet.parameterChanges
184
196
  .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
197
+ .sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
198
+
185
199
  for (const parameterChange of statefulParameterChanges) {
186
200
  const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
187
201
  await statefulParameter.applyRemove(parameterChange.previousValue, plan);
@@ -191,58 +205,6 @@ export abstract class Resource<T extends StringIndexedObject> {
191
205
  await this.applyDestroy(plan);
192
206
  }
193
207
 
194
- private initializeParameterConfigurations(
195
- resourceConfiguration: ResourceConfiguration<T>
196
- ): Record<keyof T, ParameterConfiguration> {
197
- const resourceParameters = Object.fromEntries(
198
- Object.entries(resourceConfiguration.parameterConfigurations ?? {})
199
- ?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }]))
200
- ) as Record<keyof T, ParameterConfiguration>
201
-
202
- const statefulParameters = resourceConfiguration.statefulParameters
203
- ?.reduce((obj, sp) => {
204
- return {
205
- ...obj,
206
- [sp.name]: {
207
- ...sp.configuration,
208
- isStatefulParameter: true,
209
- }
210
- }
211
- }, {}) ?? {}
212
-
213
- return {
214
- ...resourceParameters,
215
- ...statefulParameters,
216
- }
217
- }
218
-
219
- private initializeDefaultValues(
220
- resourceConfiguration: ResourceConfiguration<T>
221
- ): Partial<Record<keyof T, unknown>> {
222
- if (!resourceConfiguration.parameterConfigurations) {
223
- return {};
224
- }
225
-
226
- return Object.fromEntries(
227
- Object.entries(resourceConfiguration.parameterConfigurations)
228
- .filter((p) => p[1]?.defaultValue !== undefined)
229
- .map((config) => [config[0], config[1]!.defaultValue])
230
- ) as Partial<Record<keyof T, unknown>>;
231
- }
232
-
233
- private validateResourceConfiguration(data: ResourceConfiguration<T>) {
234
- // Stateful parameters are configured within the object not in the resource.
235
- if (data.parameterConfigurations && data.statefulParameters) {
236
- const parameters = [...Object.keys(data.parameterConfigurations)];
237
- const statefulParameterSet = new Set(data.statefulParameters.map((sp) => sp.name));
238
-
239
- const intersection = parameters.some((p) => statefulParameterSet.has(p));
240
- if (intersection) {
241
- throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
242
- }
243
- }
244
- }
245
-
246
208
  private validateRefreshResults(refresh: Partial<T> | null, desiredMap: Map<keyof T, T[keyof T]>) {
247
209
  if (!refresh) {
248
210
  return;
@@ -253,7 +215,7 @@ export abstract class Resource<T extends StringIndexedObject> {
253
215
 
254
216
  if (!setsEqual(desiredKeys, refreshKeys)) {
255
217
  throw new Error(
256
- `Resource ${this.configuration.type}
218
+ `Resource ${this.typeId}
257
219
  refresh() must return back exactly the keys that were provided
258
220
  Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
259
221
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
@@ -262,7 +224,10 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
262
224
  }
263
225
 
264
226
  private async applyTransformParameters(desired: Partial<T>): Promise<void> {
265
- for (const [key, tp] of this.transformParameters.entries()) {
227
+ const orderedEntries = [...this.transformParameters.entries()]
228
+ .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
229
+
230
+ for (const [key, tp] of orderedEntries) {
266
231
  if (desired[key] !== null) {
267
232
  const transformedValue = await tp.transform(desired[key]);
268
233
 
@@ -290,7 +255,59 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
290
255
  });
291
256
  }
292
257
 
293
- abstract validate(parameters: unknown): Promise<ValidationResult>;
258
+ private async refreshResourceParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
259
+ const entriesToRefresh = new Map(Object.entries(resourceParameters));
260
+ const currentParameters = await this.refresh(entriesToRefresh);
261
+
262
+ this.validateRefreshResults(currentParameters, entriesToRefresh);
263
+ return currentParameters;
264
+ }
265
+
266
+ // Refresh stateful parameters
267
+ // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
268
+ private async refreshStatefulParameters(statefulParametersConfig: Partial<T>, isStatefulMode: boolean): Promise<Partial<T>> {
269
+ const currentParameters: Partial<T> = {}
270
+ const sortedEntries = Object.entries(statefulParametersConfig)
271
+ .sort(([key1], [key2]) => this.statefulParameterOrder.get(key1)! - this.statefulParameterOrder.get(key2)!)
272
+
273
+ for(const [key, desiredValue] of sortedEntries) {
274
+ const statefulParameter = this.statefulParameters.get(key);
275
+ if (!statefulParameter) {
276
+ throw new Error(`Stateful parameter ${key} was not found`);
277
+ }
278
+
279
+ let currentValue = await statefulParameter.refresh(desiredValue ?? null);
280
+
281
+ // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
282
+ // Otherwise the change set will pick up the extra keys from the current and try to delete them
283
+ // This allows arrays within stateful parameters to be first class objects
284
+ if (Array.isArray(currentValue)
285
+ && Array.isArray(desiredValue)
286
+ && !isStatefulMode
287
+ && !statefulParameter.options.disableStatelessModeArrayFiltering
288
+ ) {
289
+ currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
290
+ const parameterOptions = statefulParameter.options as any;
291
+ if (parameterOptions && parameterOptions.isElementEqual) {
292
+ return parameterOptions.isElementEqual(d, c);
293
+ }
294
+
295
+ return d === c;
296
+ })) as any;
297
+ }
298
+
299
+ // @ts-ignore
300
+ currentParameters[key] = currentValue;
301
+ }
302
+
303
+ return currentParameters;
304
+ }
305
+
306
+ async validate(parameters: unknown): Promise<ValidationResult> {
307
+ return {
308
+ isValid: true,
309
+ }
310
+ };
294
311
 
295
312
  abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
296
313
 
@@ -300,3 +317,53 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
300
317
 
301
318
  abstract applyDestroy(plan: Plan<T>): Promise<void>;
302
319
  }
320
+
321
+ class ConfigParser<T extends StringIndexedObject> {
322
+ private config: Partial<T> & ResourceConfig;
323
+ private statefulParametersMap: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
324
+ private transformParametersMap: Map<keyof T, TransformParameter<T>>;
325
+
326
+ constructor(
327
+ config: Partial<T> & ResourceConfig,
328
+ statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>,
329
+ transformParameters: Map<keyof T, TransformParameter<T>>,
330
+ ) {
331
+ this.config = config;
332
+ this.statefulParametersMap = statefulParameters;
333
+ this.transformParametersMap = transformParameters;
334
+ }
335
+
336
+ get resourceMetadata(): ResourceConfig {
337
+ const { resourceMetadata } = splitUserConfig(this.config);
338
+ return resourceMetadata;
339
+ }
340
+
341
+ get parameters(): Partial<T> {
342
+ const { parameters } = splitUserConfig(this.config);
343
+ return parameters;
344
+ }
345
+
346
+ get resourceParameters(): Partial<T> {
347
+ const parameters = this.parameters;
348
+
349
+ return Object.fromEntries([
350
+ ...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
351
+ ]) as Partial<T>;
352
+ }
353
+
354
+ get statefulParameters(): Partial<T> {
355
+ const parameters = this.parameters;
356
+
357
+ return Object.fromEntries([
358
+ ...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
359
+ ]) as Partial<T>;
360
+ }
361
+
362
+ get transformParameters(): Partial<T> {
363
+ const parameters = this.parameters;
364
+
365
+ return Object.fromEntries([
366
+ ...Object.entries(parameters).filter(([key]) => this.transformParametersMap.has(key)),
367
+ ]) as Partial<T>;
368
+ }
369
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { ArrayStatefulParameter, ArrayStatefulParameterConfiguration, } from './stateful-parameter.js';
2
+ import { ArrayStatefulParameter, ArrayStatefulParameterOptions, } from './stateful-parameter.js';
3
3
  import { Plan } from './plan.js';
4
4
  import { spy } from 'sinon';
5
5
  import { ParameterOperation, ResourceOperation } from 'codify-schemas';
@@ -10,8 +10,8 @@ interface TestConfig {
10
10
  }
11
11
 
12
12
  class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
13
- constructor(configuration?: ArrayStatefulParameterConfiguration<TestConfig>) {
14
- super(configuration ?? {
13
+ constructor(options?: ArrayStatefulParameterOptions<TestConfig>) {
14
+ super(options ?? {
15
15
  name: 'propA'
16
16
  })
17
17
  }
@@ -67,7 +67,7 @@ describe('Stateful parameter tests', () => {
67
67
  { propA: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
68
68
  { propA: ['a', 'b', 'c'] },
69
69
  { type: 'typeA' },
70
- { statefulMode: true, parameterConfigurations: { propA: { isStatefulParameter: true }} }
70
+ { statefulMode: true, parameterOptions: { propA: { isStatefulParameter: true }} }
71
71
  );
72
72
 
73
73
  expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
@@ -90,7 +90,7 @@ describe('Stateful parameter tests', () => {
90
90
  { propA: ['9.12', '9.13'] }, // b to remove, d, e, f to add
91
91
  { propA: ['9.12.9'] },
92
92
  { type: 'typeA' },
93
- { statefulMode: false, parameterConfigurations: { propA: { isStatefulParameter: true }} }
93
+ { statefulMode: false, parameterOptions: { propA: { isStatefulParameter: true }} }
94
94
  );
95
95
 
96
96
  expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
@@ -1,7 +1,7 @@
1
1
  import { Plan } from './plan.js';
2
2
  import { StringIndexedObject } from 'codify-schemas';
3
3
 
4
- export interface StatefulParameterConfiguration<T> {
4
+ export interface StatefulParameterOptions<T> {
5
5
  name: keyof T;
6
6
  isEqual?: (desired: any, current: any) => boolean;
7
7
 
@@ -17,7 +17,7 @@ export interface StatefulParameterConfiguration<T> {
17
17
  disableStatelessModeArrayFiltering?: boolean;
18
18
  }
19
19
 
20
- export interface ArrayStatefulParameterConfiguration<T> extends StatefulParameterConfiguration<T> {
20
+ export interface ArrayStatefulParameterOptions<T> extends StatefulParameterOptions<T> {
21
21
  isEqual?: (desired: any[], current: any[]) => boolean;
22
22
  isElementEqual?: (desired: any, current: any) => boolean;
23
23
  }
@@ -25,11 +25,11 @@ export interface ArrayStatefulParameterConfiguration<T> extends StatefulParamete
25
25
 
26
26
  export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
27
27
  readonly name: keyof T;
28
- readonly configuration: StatefulParameterConfiguration<T>;
28
+ readonly options: StatefulParameterOptions<T>;
29
29
 
30
- protected constructor(configuration: StatefulParameterConfiguration<T>) {
31
- this.name = configuration.name;
32
- this.configuration = configuration
30
+ protected constructor(options: StatefulParameterOptions<T>) {
31
+ this.name = options.name;
32
+ this.options = options
33
33
  }
34
34
 
35
35
  abstract refresh(desired: V | null): Promise<V | null>;
@@ -41,11 +41,11 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
41
41
  }
42
42
 
43
43
  export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
44
- configuration: ArrayStatefulParameterConfiguration<T>;
44
+ options: ArrayStatefulParameterOptions<T>;
45
45
 
46
- constructor(configuration: ArrayStatefulParameterConfiguration<T>) {
47
- super(configuration);
48
- this.configuration = configuration;
46
+ constructor(options: ArrayStatefulParameterOptions<T>) {
47
+ super(options);
48
+ this.options = options;
49
49
  }
50
50
 
51
51
  async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
@@ -55,18 +55,18 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
55
55
  }
56
56
 
57
57
  async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
58
- const configuration = this.configuration as ArrayStatefulParameterConfiguration<T>;
58
+ const options = this.options as ArrayStatefulParameterOptions<T>;
59
59
 
60
60
  const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
61
- if ((configuration).isElementEqual) {
62
- return configuration.isElementEqual(n, p);
61
+ if (options.isElementEqual) {
62
+ return options.isElementEqual(n, p);
63
63
  }
64
64
  return n === p;
65
65
  }));
66
66
 
67
67
  const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
68
- if ((configuration).isElementEqual) {
69
- return configuration.isElementEqual(n, p);
68
+ if (options.isElementEqual) {
69
+ return options.isElementEqual(n, p);
70
70
  }
71
71
  return n === p;
72
72
  }));