codify-plugin-lib 1.0.76 → 1.0.77

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.
Files changed (70) hide show
  1. package/.eslintrc.json +11 -4
  2. package/.github/workflows/release.yaml +19 -0
  3. package/.github/workflows/unit-test-ci.yaml +19 -0
  4. package/dist/errors.d.ts +4 -0
  5. package/dist/errors.js +7 -0
  6. package/dist/index.d.ts +10 -10
  7. package/dist/index.js +9 -9
  8. package/dist/messages/handlers.d.ts +1 -1
  9. package/dist/messages/handlers.js +2 -1
  10. package/dist/plan/change-set.d.ts +37 -0
  11. package/dist/plan/change-set.js +146 -0
  12. package/dist/plan/plan-types.d.ts +23 -0
  13. package/dist/plan/plan-types.js +1 -0
  14. package/dist/plan/plan.d.ts +59 -0
  15. package/dist/plan/plan.js +228 -0
  16. package/dist/plugin/plugin.d.ts +17 -0
  17. package/dist/plugin/plugin.js +83 -0
  18. package/dist/resource/config-parser.d.ts +14 -0
  19. package/dist/resource/config-parser.js +48 -0
  20. package/dist/resource/parsed-resource-settings.d.ts +26 -0
  21. package/dist/resource/parsed-resource-settings.js +126 -0
  22. package/dist/resource/resource-controller.d.ts +30 -0
  23. package/dist/resource/resource-controller.js +247 -0
  24. package/dist/resource/resource-settings.d.ts +149 -0
  25. package/dist/resource/resource-settings.js +9 -0
  26. package/dist/resource/resource.d.ts +137 -0
  27. package/dist/resource/resource.js +44 -0
  28. package/dist/resource/stateful-parameter.d.ts +164 -0
  29. package/dist/resource/stateful-parameter.js +94 -0
  30. package/dist/utils/utils.d.ts +19 -3
  31. package/dist/utils/utils.js +52 -3
  32. package/package.json +5 -3
  33. package/src/index.ts +10 -11
  34. package/src/messages/handlers.test.ts +10 -37
  35. package/src/messages/handlers.ts +2 -2
  36. package/src/plan/change-set.test.ts +220 -0
  37. package/src/plan/change-set.ts +225 -0
  38. package/src/plan/plan-types.ts +27 -0
  39. package/src/{entities → plan}/plan.test.ts +35 -29
  40. package/src/plan/plan.ts +353 -0
  41. package/src/{entities → plugin}/plugin.test.ts +14 -13
  42. package/src/{entities → plugin}/plugin.ts +28 -24
  43. package/src/resource/config-parser.ts +77 -0
  44. package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
  45. package/src/resource/parsed-resource-settings.ts +179 -0
  46. package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
  47. package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
  48. package/src/resource/resource-controller.ts +340 -0
  49. package/src/resource/resource-settings.test.ts +494 -0
  50. package/src/resource/resource-settings.ts +192 -0
  51. package/src/resource/resource.ts +149 -0
  52. package/src/resource/stateful-parameter.test.ts +93 -0
  53. package/src/resource/stateful-parameter.ts +217 -0
  54. package/src/utils/test-utils.test.ts +87 -0
  55. package/src/utils/utils.test.ts +2 -2
  56. package/src/utils/utils.ts +51 -5
  57. package/tsconfig.json +0 -1
  58. package/vitest.config.ts +10 -0
  59. package/src/entities/change-set.test.ts +0 -155
  60. package/src/entities/change-set.ts +0 -244
  61. package/src/entities/plan-types.ts +0 -44
  62. package/src/entities/plan.ts +0 -178
  63. package/src/entities/resource-options.ts +0 -155
  64. package/src/entities/resource-parameters.test.ts +0 -604
  65. package/src/entities/resource-types.ts +0 -31
  66. package/src/entities/resource.ts +0 -470
  67. package/src/entities/stateful-parameter.test.ts +0 -114
  68. package/src/entities/stateful-parameter.ts +0 -92
  69. package/src/entities/transform-parameter.ts +0 -13
  70. /package/src/{entities/errors.ts → errors.ts} +0 -0
@@ -1,470 +0,0 @@
1
- import { Ajv, ValidateFunction } from 'ajv';
2
- import {
3
- ParameterOperation,
4
- ResourceConfig,
5
- ResourceOperation,
6
- StringIndexedObject,
7
- ValidateResponseData,
8
- } from 'codify-schemas';
9
-
10
- import { setsEqual, splitUserConfig } from '../utils/utils.js';
11
- import { ParameterChange } from './change-set.js';
12
- import { Plan } from './plan.js';
13
- import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions, PlanOptions } from './plan-types.js';
14
- import { ResourceOptions, ResourceOptionsParser } from './resource-options.js';
15
- import { ResourceParameterOptions } from './resource-types.js';
16
- import { StatefulParameter } from './stateful-parameter.js';
17
- import { TransformParameter } from './transform-parameter.js';
18
-
19
- /**
20
- * Description of resource here
21
- * Two main functions:
22
- * - Plan
23
- * - Apply
24
- *
25
- */
26
- export abstract class Resource<T extends StringIndexedObject> {
27
- readonly typeId: string;
28
- readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
29
- readonly transformParameters: Map<keyof T, TransformParameter<T>>
30
- readonly resourceParameters: Map<keyof T, ResourceParameterOptions>;
31
-
32
- readonly statefulParameterOrder: Map<keyof T, number>;
33
- readonly transformParameterOrder: Map<keyof T, number>;
34
-
35
- readonly dependencies: string[]; // TODO: Change this to a string
36
- readonly parameterOptions: Record<keyof T, ParameterOptions>
37
- readonly options: ResourceOptions<T>;
38
- readonly defaultValues: Partial<Record<keyof T, unknown>>;
39
-
40
- protected ajv?: Ajv;
41
- protected schemaValidator?: ValidateFunction;
42
-
43
- protected constructor(options: ResourceOptions<T>) {
44
- this.typeId = options.type;
45
- this.dependencies = options.dependencies ?? [];
46
- this.options = options;
47
-
48
- if (this.options.schema) {
49
- this.ajv = new Ajv({
50
- allErrors: true,
51
- strict: true,
52
- strictRequired: false,
53
- })
54
- this.schemaValidator = this.ajv.compile(this.options.schema);
55
- }
56
-
57
- const parser = new ResourceOptionsParser<T>(options);
58
- this.statefulParameters = parser.statefulParameters;
59
- this.transformParameters = parser.transformParameters;
60
- this.resourceParameters = parser.resourceParameters;
61
- this.parameterOptions = parser.changeSetParameterOptions;
62
- this.defaultValues = parser.defaultValues;
63
- this.statefulParameterOrder = parser.statefulParameterOrder;
64
- this.transformParameterOrder = parser.transformParameterOrder;
65
- }
66
-
67
- async onInitialize(): Promise<void> {}
68
-
69
- async validate(
70
- parameters: Partial<T>,
71
- resourceMetaData: ResourceConfig
72
- ): Promise<ValidateResponseData['resourceValidations'][0]> {
73
- if (this.schemaValidator) {
74
- const isValid = this.schemaValidator(parameters);
75
-
76
- if (!isValid) {
77
- return {
78
- isValid: false,
79
- resourceName: resourceMetaData.name,
80
- resourceType: resourceMetaData.type,
81
- schemaValidationErrors: this.schemaValidator?.errors ?? [],
82
- }
83
- }
84
- }
85
-
86
- let isValid = true;
87
- let customValidationErrorMessage;
88
- try {
89
- await this.customValidation(parameters);
90
- } catch (error) {
91
- isValid = false;
92
- customValidationErrorMessage = (error as Error).message;
93
- }
94
-
95
- if (!isValid) {
96
- return {
97
- customValidationErrorMessage,
98
- isValid: false,
99
- resourceName: resourceMetaData.name,
100
- resourceType: resourceMetaData.type,
101
- schemaValidationErrors: this.schemaValidator?.errors ?? [],
102
- }
103
- }
104
-
105
- return {
106
- isValid: true,
107
- resourceName: resourceMetaData.name,
108
- resourceType: resourceMetaData.type,
109
- schemaValidationErrors: [],
110
- }
111
- }
112
-
113
- // TODO: Currently stateful mode expects that the currentConfig does not need any additional transformations (default and transform parameters)
114
- // This may change in the future?
115
- async plan(
116
- desiredConfig: Partial<T> & ResourceConfig | null,
117
- currentConfig: Partial<T> & ResourceConfig | null = null,
118
- statefulMode = false,
119
- ): Promise<Plan<T>> {
120
- this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
121
-
122
- const planOptions: PlanOptions<T> = {
123
- parameterOptions: this.parameterOptions,
124
- statefulMode,
125
- }
126
-
127
- this.addDefaultValues(desiredConfig);
128
- await this.applyTransformParameters(desiredConfig);
129
-
130
- // Parse data from the user supplied config
131
- const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters)
132
- const {
133
- desiredParameters,
134
- nonStatefulParameters,
135
- resourceMetadata,
136
- statefulParameters,
137
- } = parsedConfig;
138
-
139
- // Refresh resource parameters. This refreshes the parameters that configure the resource itself
140
- const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
141
-
142
- // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
143
- if (currentParameters === null || currentParameters === undefined) {
144
- return Plan.create(
145
- desiredParameters,
146
- null,
147
- resourceMetadata,
148
- planOptions,
149
- );
150
- }
151
-
152
- // Refresh stateful parameters. These parameters have state external to the resource
153
- const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
154
-
155
- return Plan.create(
156
- desiredParameters,
157
- { ...currentParameters, ...statefulCurrentParameters } as Partial<T>,
158
- resourceMetadata,
159
- planOptions,
160
- )
161
- }
162
-
163
- async apply(plan: Plan<T>): Promise<void> {
164
- if (plan.getResourceType() !== this.typeId) {
165
- throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
166
- }
167
-
168
- switch (plan.changeSet.operation) {
169
- case ResourceOperation.CREATE: {
170
- return this._applyCreate(plan); // TODO: Add new parameters value so that apply
171
- }
172
-
173
- case ResourceOperation.MODIFY: {
174
- return this._applyModify(plan);
175
- }
176
-
177
- case ResourceOperation.RECREATE: {
178
- await this._applyDestroy(plan);
179
- return this._applyCreate(plan);
180
- }
181
-
182
- case ResourceOperation.DESTROY: {
183
- return this._applyDestroy(plan);
184
- }
185
- }
186
- }
187
-
188
- private async _applyCreate(plan: Plan<T>): Promise<void> {
189
- await this.applyCreate(plan as CreatePlan<T>);
190
-
191
- const statefulParameterChanges = plan.changeSet.parameterChanges
192
- .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
193
- .sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
194
-
195
- for (const parameterChange of statefulParameterChanges) {
196
- const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
197
- await statefulParameter.applyAdd(parameterChange.newValue, plan);
198
- }
199
- }
200
-
201
- private async _applyModify(plan: Plan<T>): Promise<void> {
202
- const parameterChanges = plan
203
- .changeSet
204
- .parameterChanges
205
- .filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
206
-
207
- const statelessParameterChanges = parameterChanges
208
- .filter((pc: ParameterChange<T>) => !this.statefulParameters.has(pc.name))
209
-
210
- for (const pc of statelessParameterChanges) {
211
- // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
212
- await this.applyModify(pc, plan as ModifyPlan<T>);
213
- }
214
-
215
- const statefulParameterChanges = parameterChanges
216
- .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
217
- .sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
218
-
219
- for (const parameterChange of statefulParameterChanges) {
220
- const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
221
-
222
- switch (parameterChange.operation) {
223
- case ParameterOperation.ADD: {
224
- await statefulParameter.applyAdd(parameterChange.newValue, plan);
225
- break;
226
- }
227
-
228
- case ParameterOperation.MODIFY: {
229
- // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
230
- await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
231
- break;
232
- }
233
-
234
- case ParameterOperation.REMOVE: {
235
- await statefulParameter.applyRemove(parameterChange.previousValue, plan);
236
- break;
237
- }
238
- }
239
- }
240
- }
241
-
242
- private async _applyDestroy(plan: Plan<T>): Promise<void> {
243
- // If this option is set (defaults to false), then stateful parameters need to be destroyed
244
- // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
245
- if (this.options.callStatefulParameterRemoveOnDestroy) {
246
- const statefulParameterChanges = plan.changeSet.parameterChanges
247
- .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
248
- .sort((a, b) => this.statefulParameterOrder.get(a.name)! - this.statefulParameterOrder.get(b.name)!)
249
-
250
- for (const parameterChange of statefulParameterChanges) {
251
- const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
252
- await statefulParameter.applyRemove(parameterChange.previousValue, plan);
253
- }
254
- }
255
-
256
- await this.applyDestroy(plan as DestroyPlan<T>);
257
- }
258
-
259
- private validateRefreshResults(refresh: Partial<T> | null, desired: Partial<T>) {
260
- if (!refresh) {
261
- return;
262
- }
263
-
264
- const desiredKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
265
- const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
266
-
267
- if (!setsEqual(desiredKeys, refreshKeys)) {
268
- throw new Error(
269
- `Resource ${this.typeId}
270
- refresh() must return back exactly the keys that were provided
271
- Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
272
- Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
273
- );
274
- }
275
- }
276
-
277
- private async applyTransformParameters(desired: Partial<T> | null): Promise<void> {
278
- if (!desired) {
279
- return;
280
- }
281
-
282
- const transformParameters = [...this.transformParameters.entries()]
283
- .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
284
-
285
- for (const [key, transformParameter] of transformParameters) {
286
- if (desired[key] === undefined) {
287
- continue;
288
- }
289
-
290
- const transformedValue = await transformParameter.transform(desired[key]);
291
-
292
- if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
293
- throw new Error(`Transform parameter ${key as string} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
294
- }
295
-
296
- // Remove original transform parameter from the config
297
- delete desired[key];
298
-
299
- // Add the new transformed values
300
- for (const [tvKey, tvValue] of Object.entries(transformedValue)) {
301
- // @ts-ignore
302
- desired[tvKey] = tvValue;
303
- }
304
- }
305
- }
306
-
307
- private addDefaultValues(desired: Partial<T> | null): void {
308
- if (!desired) {
309
- return;
310
- }
311
-
312
- for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
313
- if (defaultValue !== undefined && desired[key as any] === undefined) {
314
- // @ts-ignore
315
- desired[key] = defaultValue;
316
- }
317
- }
318
- }
319
-
320
- private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
321
- const currentParameters = await this.refresh(resourceParameters);
322
- this.validateRefreshResults(currentParameters, resourceParameters);
323
- return currentParameters;
324
- }
325
-
326
- // Refresh stateful parameters
327
- // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
328
- private async refreshStatefulParameters(statefulParametersConfig: Partial<T>, isStatefulMode: boolean): Promise<Partial<T>> {
329
- const currentParameters: Partial<T> = {}
330
- const sortedEntries = Object.entries(statefulParametersConfig)
331
- .sort(([key1], [key2]) => this.statefulParameterOrder.get(key1)! - this.statefulParameterOrder.get(key2)!)
332
-
333
- for(const [key, desiredValue] of sortedEntries) {
334
- const statefulParameter = this.statefulParameters.get(key);
335
- if (!statefulParameter) {
336
- throw new Error(`Stateful parameter ${key} was not found`);
337
- }
338
-
339
- let currentValue = await statefulParameter.refresh(desiredValue ?? null);
340
-
341
- // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
342
- // Otherwise the change set will pick up the extra keys from the current and try to delete them
343
- // This allows arrays within stateful parameters to be first class objects
344
- if (Array.isArray(currentValue)
345
- && Array.isArray(desiredValue)
346
- && !isStatefulMode
347
- && !statefulParameter.options.disableStatelessModeArrayFiltering
348
- ) {
349
- currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
350
- const parameterOptions = statefulParameter.options as any;
351
- if (parameterOptions && parameterOptions.isElementEqual) {
352
- return parameterOptions.isElementEqual(d, c);
353
- }
354
-
355
- return d === c;
356
- })) as any;
357
- }
358
-
359
- // @ts-ignore
360
- currentParameters[key] = currentValue;
361
- }
362
-
363
- return currentParameters;
364
- }
365
-
366
- private validatePlanInputs(
367
- desired: Partial<T> & ResourceConfig | null,
368
- current: Partial<T> & ResourceConfig | null,
369
- statefulMode: boolean,
370
- ) {
371
- if (!desired && !current) {
372
- throw new Error('Desired config and current config cannot both be missing')
373
- }
374
-
375
- if (!statefulMode && !desired) {
376
- throw new Error('Desired config must be provided in non-stateful mode')
377
- }
378
- }
379
-
380
- /**
381
- * Add custom validation logic in-addition to the default schema validation.
382
- * In this method throw an error if the object did not validate. The message of the
383
- * error will be shown to the user.
384
- * @param parameters
385
- */
386
- async customValidation(parameters: Partial<T>): Promise<void> {};
387
-
388
- abstract refresh(parameters: Partial<T>): Promise<Partial<T> | null>;
389
-
390
- abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
391
-
392
- async applyModify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void> {};
393
-
394
- abstract applyDestroy(plan: DestroyPlan<T>): Promise<void>;
395
- }
396
-
397
- class ConfigParser<T extends StringIndexedObject> {
398
- private desiredConfig: Partial<T> & ResourceConfig | null;
399
- private currentConfig: Partial<T> & ResourceConfig | null;
400
- private statefulParametersMap: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
401
- private transformParametersMap: Map<keyof T, TransformParameter<T>>;
402
-
403
- constructor(
404
- desiredConfig: Partial<T> & ResourceConfig | null,
405
- currentConfig: Partial<T> & ResourceConfig | null,
406
- statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>,
407
- transformParameters: Map<keyof T, TransformParameter<T>>,
408
- ) {
409
- this.desiredConfig = desiredConfig;
410
- this.currentConfig = currentConfig
411
- this.statefulParametersMap = statefulParameters;
412
- this.transformParametersMap = transformParameters;
413
- }
414
-
415
- get resourceMetadata(): ResourceConfig {
416
- const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
417
- const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
418
-
419
- if (!desiredMetadata && !currentMetadata) {
420
- throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`)
421
- }
422
-
423
- if (currentMetadata && desiredMetadata && (
424
- Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
425
- || Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value)
426
- )) {
427
- throw new Error(`The metadata for the current config does not match the desired config.
428
- Desired metadata:
429
- ${JSON.stringify(desiredMetadata, null, 2)}
430
-
431
- Current metadata:
432
- ${JSON.stringify(currentMetadata, null, 2)}`);
433
- }
434
-
435
- return desiredMetadata ?? currentMetadata!;
436
- }
437
-
438
- get desiredParameters(): Partial<T> | null {
439
- if (!this.desiredConfig) {
440
- return null;
441
- }
442
-
443
- const { parameters } = splitUserConfig(this.desiredConfig);
444
- return parameters;
445
- }
446
-
447
-
448
- get parameters(): Partial<T> {
449
- const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
450
- const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
451
-
452
- return { ...desiredParameters, ...currentParameters } as Partial<T>;
453
- }
454
-
455
- get nonStatefulParameters(): Partial<T> {
456
- const { parameters } = this;
457
-
458
- return Object.fromEntries(
459
- Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key)))
460
- ) as Partial<T>;
461
- }
462
-
463
- get statefulParameters(): Partial<T> {
464
- const { parameters } = this;
465
-
466
- return Object.fromEntries(
467
- Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key))
468
- ) as Partial<T>;
469
- }
470
- }
@@ -1,114 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { ArrayStatefulParameter, ArrayStatefulParameterOptions, } 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(options?: ArrayStatefulParameterOptions<string>) {
14
- super(options)
15
- }
16
-
17
- async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
18
- async applyRemoveItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
19
-
20
- async refresh(): Promise<string[] | null> {
21
- return null;
22
- }
23
- }
24
-
25
- describe('Stateful parameter tests', () => {
26
- it('applyAddItem is called the correct number of times', async () => {
27
- const plan = Plan.create<TestConfig>(
28
- { propA: ['a', 'b', 'c'] },
29
- null,
30
- { type: 'typeA' },
31
- { statefulMode: false }
32
- );
33
-
34
- expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
35
- expect(plan.changeSet.parameterChanges.length).to.eq(1);
36
-
37
- const testParameter = spy(new TestArrayParameter());
38
- await testParameter.applyAdd(plan.desiredConfig!.propA, plan);
39
-
40
- expect(testParameter.applyAddItem.callCount).to.eq(3);
41
- expect(testParameter.applyRemoveItem.called).to.be.false;
42
- })
43
-
44
- it('applyRemoveItem is called the correct number of times', async () => {
45
- const plan = Plan.create<TestConfig>(
46
- null,
47
- { propA: ['a', 'b', 'c'] },
48
- { type: 'typeA' },
49
- { statefulMode: true }
50
- );
51
-
52
- expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
53
- expect(plan.changeSet.parameterChanges.length).to.eq(1);
54
-
55
- const testParameter = spy(new TestArrayParameter());
56
- await testParameter.applyRemove(plan.currentConfig!.propA, plan);
57
-
58
- expect(testParameter.applyAddItem.called).to.be.false;
59
- expect(testParameter.applyRemoveItem.callCount).to.eq(3);
60
- })
61
-
62
- it('In stateless mode only applyAddItem is called only for modifies', async () => {
63
- const plan = Plan.create<TestConfig>(
64
- { propA: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
65
- { propA: ['a', 'b', 'c'] },
66
- { type: 'typeA' },
67
- { statefulMode: true, parameterOptions: { propA: { isStatefulParameter: true }} }
68
- );
69
-
70
- expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
71
- expect(plan.changeSet.parameterChanges[0]).toMatchObject({
72
- name: 'propA',
73
- previousValue: ['a', 'b', 'c'],
74
- newValue: ['a', 'c', 'd', 'e', 'f'],
75
- operation: ParameterOperation.MODIFY,
76
- })
77
-
78
- const testParameter = spy(new TestArrayParameter());
79
- await testParameter.applyModify(plan.desiredConfig!.propA, plan.currentConfig!.propA, false, plan);
80
-
81
- expect(testParameter.applyAddItem.calledThrice).to.be.true;
82
- expect(testParameter.applyRemoveItem.called).to.be.false;
83
- })
84
-
85
- it('isElementEqual is called for modifies', async () => {
86
- const plan = Plan.create<TestConfig>(
87
- { propA: ['9.12', '9.13'] }, // b to remove, d, e, f to add
88
- { propA: ['9.12.9'] },
89
- { type: 'typeA' },
90
- { statefulMode: false, parameterOptions: { propA: { isStatefulParameter: true }} }
91
- );
92
-
93
- expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
94
- expect(plan.changeSet.parameterChanges[0]).toMatchObject({
95
- name: 'propA',
96
- previousValue: ['9.12.9'],
97
- newValue: ['9.12', '9.13'],
98
- operation: ParameterOperation.MODIFY,
99
- })
100
-
101
- const testParameter = spy(new class extends TestArrayParameter {
102
- constructor() {
103
- super({
104
- isElementEqual: (desired, current) => current.includes(desired),
105
- });
106
- }
107
- });
108
-
109
- await testParameter.applyModify(plan.desiredConfig!.propA, plan.currentConfig!.propA, false, plan);
110
-
111
- expect(testParameter.applyAddItem.calledOnce).to.be.true;
112
- expect(testParameter.applyRemoveItem.called).to.be.false;
113
- })
114
- })
@@ -1,92 +0,0 @@
1
- import { Plan } from './plan.js';
2
- import { StringIndexedObject } from 'codify-schemas';
3
-
4
- export interface StatefulParameterOptions<V> {
5
- isEqual?: (desired: any, current: any) => boolean;
6
-
7
- /**
8
- * In stateless mode, array refresh results (current) will be automatically filtered by the user config (desired).
9
- * This is done to ensure that for modify operations, stateless mode will not try to delete existing resources.
10
- *
11
- * Ex: System has python 3.11.9 and 3.12.7 installed (current). Desired is 3.11. Without filtering 3.12.7 will be deleted
12
- * in the next modify
13
- *
14
- * Set this flag to true to disable this behaviour
15
- */
16
- disableStatelessModeArrayFiltering?: boolean;
17
- default?: V;
18
- }
19
-
20
- export interface ArrayStatefulParameterOptions<V> extends StatefulParameterOptions<V> {
21
- isEqual?: (desired: any[], current: any[]) => boolean;
22
- isElementEqual?: (desired: any, current: any) => boolean;
23
- }
24
-
25
-
26
- export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
27
- readonly options: StatefulParameterOptions<V>;
28
-
29
- constructor(options: StatefulParameterOptions<V> = {}) {
30
- this.options = options
31
- }
32
-
33
- abstract refresh(desired: V | null): Promise<V | null>;
34
-
35
- // TODO: Add an additional parameter here for what has actually changed.
36
- abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
37
- abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
38
- abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
39
- }
40
-
41
- export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
42
- options: ArrayStatefulParameterOptions<V>;
43
-
44
- constructor(options: ArrayStatefulParameterOptions<V> = {}) {
45
- super(options);
46
- this.options = options;
47
- }
48
-
49
- async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
50
- for (const value of valuesToAdd) {
51
- await this.applyAddItem(value, plan);
52
- }
53
- }
54
-
55
- async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
56
- const options = this.options as ArrayStatefulParameterOptions<V>;
57
-
58
- const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
59
- if (options.isElementEqual) {
60
- return options.isElementEqual(n, p);
61
- }
62
- return n === p;
63
- }));
64
-
65
- const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
66
- if (options.isElementEqual) {
67
- return options.isElementEqual(n, p);
68
- }
69
- return n === p;
70
- }));
71
-
72
- for (const value of valuesToAdd) {
73
- await this.applyAddItem(value, plan)
74
- }
75
-
76
- if (allowDeletes) {
77
- for (const value of valuesToRemove) {
78
- await this.applyRemoveItem(value, plan)
79
- }
80
- }
81
- }
82
-
83
- async applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
84
- for (const value of valuesToRemove) {
85
- await this.applyRemoveItem(value as V, plan);
86
- }
87
- }
88
-
89
- abstract refresh(desired: V[] | null): Promise<V[] | null>;
90
- abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
91
- abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
92
- }
@@ -1,13 +0,0 @@
1
- import { StringIndexedObject } from 'codify-schemas';
2
-
3
- /**
4
- * Transform parameters convert the provided value into
5
- * other parameters. Transform parameters will not show up
6
- * in the refresh or the plan. Transform parameters get processed after
7
- * default values.
8
- */
9
- export abstract class TransformParameter<T extends StringIndexedObject> {
10
-
11
- abstract transform(value: any): Promise<Partial<T>>
12
-
13
- }
File without changes