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