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
@@ -0,0 +1,494 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Plan } from '../plan/plan.js';
3
+ import { spy } from 'sinon';
4
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
5
+ import {
6
+ TestArrayStatefulParameter,
7
+ TestConfig,
8
+ testPlan,
9
+ TestResource,
10
+ TestStatefulParameter
11
+ } from '../utils/test-utils.test.js';
12
+ import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
13
+ import { ResourceController } from './resource-controller.js';
14
+
15
+ describe('Resource parameter tests', () => {
16
+ it('Generates a resource plan that includes stateful parameters (create)', async () => {
17
+ const statefulParameter = spy(new class extends TestStatefulParameter {
18
+ async refresh(): Promise<string | null> {
19
+ return null;
20
+ }
21
+ })
22
+
23
+ const resource = new class extends TestResource {
24
+ getSettings(): ResourceSettings<TestConfig> {
25
+ return {
26
+ id: 'type',
27
+ parameterSettings: {
28
+ propA: { type: 'stateful', definition: statefulParameter }
29
+ },
30
+ };
31
+ }
32
+
33
+ async refresh(): Promise<any> {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ const controller = new ResourceController(resource);
39
+ const plan = await controller.plan({
40
+ type: 'type',
41
+ propA: 'a',
42
+ propB: 10
43
+ })
44
+
45
+ expect(statefulParameter.refresh.notCalled).to.be.true;
46
+ expect(plan.currentConfig).to.be.null;
47
+ expect(plan.desiredConfig).toMatchObject({
48
+ type: 'type',
49
+ propA: 'a',
50
+ propB: 10
51
+ })
52
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
53
+ })
54
+
55
+ it('supports the creation of stateful parameters', async () => {
56
+
57
+ const statefulParameter = new class extends TestStatefulParameter {
58
+ async refresh(): Promise<string | null> {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ const statefulParameterSpy = spy(statefulParameter);
64
+
65
+ const resource = new class extends TestResource {
66
+ getSettings(): ResourceSettings<TestConfig> {
67
+ return {
68
+ id: 'type',
69
+ parameterSettings: {
70
+ propA: { type: 'stateful', definition: statefulParameterSpy }
71
+ },
72
+ }
73
+ }
74
+ }
75
+
76
+ const controller = new ResourceController(resource);
77
+ const resourceSpy = spy(resource);
78
+
79
+ await controller.apply(
80
+ testPlan<TestConfig>({
81
+ desired: { propA: 'a', propB: 0, propC: 'c' }
82
+ })
83
+ );
84
+
85
+ expect(statefulParameterSpy.add.calledOnce).to.be.true;
86
+ expect(resourceSpy.create.calledOnce).to.be.true;
87
+ })
88
+
89
+ it('supports the modification of stateful parameters', async () => {
90
+ const statefulParameter = new class extends TestStatefulParameter {
91
+ async refresh(): Promise<string | null> {
92
+ return 'b';
93
+ }
94
+ }
95
+
96
+ const statefulParameterSpy = spy(statefulParameter);
97
+
98
+ const resource = new class extends TestResource {
99
+
100
+ getSettings(): ResourceSettings<TestConfig> {
101
+ return {
102
+ id: 'type',
103
+ parameterSettings: {
104
+ propA: { type: 'stateful', definition: statefulParameterSpy },
105
+ propB: { canModify: true },
106
+ }
107
+ };
108
+ }
109
+
110
+ async refresh(): Promise<Partial<TestConfig> | null> {
111
+ return { propB: -1, propC: 'b' }
112
+ }
113
+ }
114
+
115
+ const controller = new ResourceController(resource);
116
+
117
+ const plan = await controller.plan({ type: 'type', propA: 'a', propB: 0, propC: 'b' })
118
+
119
+ const resourceSpy = spy(resource);
120
+ await controller.apply(plan);
121
+
122
+ expect(statefulParameterSpy.modify.calledOnce).to.be.true;
123
+ expect(resourceSpy.modify.calledOnce).to.be.true;
124
+ })
125
+
126
+ it('Allows stateful parameters to have default values', async () => {
127
+ const statefulParameter = spy(new class extends TestStatefulParameter {
128
+ getSettings(): ParameterSetting {
129
+ return {
130
+ default: 'abc'
131
+ };
132
+ }
133
+
134
+ async refresh(): Promise<string | null> {
135
+ return null;
136
+ }
137
+ });
138
+
139
+ const resource = new class extends TestResource {
140
+ getSettings(): ResourceSettings<TestConfig> {
141
+ return {
142
+ id: 'type',
143
+ parameterSettings: {
144
+ propA: { type: 'stateful', definition: statefulParameter }
145
+ },
146
+ }
147
+ }
148
+
149
+ async refresh(): Promise<any> {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ const controller = new ResourceController(resource);
155
+ const plan = await controller.plan({
156
+ type: 'type',
157
+ })
158
+
159
+ expect(statefulParameter.refresh.notCalled).to.be.true;
160
+ expect(plan.currentConfig).to.be.null;
161
+ expect(plan.desiredConfig).toMatchObject({
162
+ type: 'type',
163
+ propA: 'abc',
164
+ })
165
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
166
+ })
167
+
168
+ it('Filters array results in stateless mode to prevent modify from being called', async () => {
169
+ const statefulParameter = new class extends TestStatefulParameter {
170
+ getSettings(): ParameterSetting {
171
+ return { type: 'array' }
172
+ }
173
+
174
+ async refresh(): Promise<any | null> {
175
+ return ['a', 'b', 'c', 'd']
176
+ }
177
+ }
178
+
179
+ const statefulParameterSpy = spy(statefulParameter);
180
+
181
+ const resource = new class extends TestResource {
182
+ getSettings(): ResourceSettings<TestConfig> {
183
+ return {
184
+ id: 'type',
185
+ parameterSettings: {
186
+ propA: { type: 'stateful', definition: statefulParameterSpy },
187
+ },
188
+ }
189
+ }
190
+
191
+ async refresh(): Promise<Partial<TestConfig> | null> {
192
+ return {};
193
+ }
194
+ }
195
+
196
+ const controller = new ResourceController(resource);
197
+ const plan = await controller.plan({ type: 'type', propA: ['a', 'b'] } as any)
198
+
199
+ expect(plan).toMatchObject({
200
+ changeSet: {
201
+ operation: ResourceOperation.NOOP,
202
+ }
203
+ })
204
+ })
205
+
206
+ it('Filters array results in stateless mode to prevent modify from being called 2', async () => {
207
+ const statefulParameter = new class extends TestStatefulParameter {
208
+ async refresh(): Promise<any | null> {
209
+ return ['a', 'b']
210
+ }
211
+ }
212
+
213
+ const statefulParameterSpy = spy(statefulParameter);
214
+
215
+ const resource = new class extends TestResource {
216
+ getSettings(): ResourceSettings<TestConfig> {
217
+ return {
218
+ id: 'type',
219
+ parameterSettings: {
220
+ propA: { type: 'stateful', definition: statefulParameterSpy }
221
+ },
222
+ }
223
+ }
224
+
225
+ async refresh(): Promise<Partial<TestConfig> | null> {
226
+ return {};
227
+ }
228
+ }
229
+
230
+ const controller = new ResourceController(resource);
231
+ const plan = await controller.plan({ type: 'type', propA: ['a', 'b', 'c', 'd'] } as any)
232
+
233
+ expect(plan).toMatchObject({
234
+ changeSet: {
235
+ operation: ResourceOperation.MODIFY,
236
+ }
237
+ })
238
+ })
239
+
240
+ it('Uses isElementEqual for stateless mode filtering if available', async () => {
241
+ const statefulParameter = new class extends TestArrayStatefulParameter {
242
+ getSettings(): ArrayParameterSetting {
243
+ return {
244
+ type: 'array',
245
+ isElementEqual: (desired, current) => {
246
+ return current.includes(desired)
247
+ },
248
+ }
249
+ }
250
+
251
+ async refresh(): Promise<any | null> {
252
+ return ['3.11.9']
253
+ }
254
+ }
255
+
256
+ const statefulParameterSpy = spy(statefulParameter);
257
+
258
+ const resource = new class extends TestResource {
259
+ getSettings(): ResourceSettings<TestConfig> {
260
+ return {
261
+ id: 'type',
262
+ parameterSettings: {
263
+ propA: { type: 'stateful', definition: statefulParameterSpy }
264
+ },
265
+ }
266
+ }
267
+
268
+ async refresh(): Promise<Partial<TestConfig> | null> {
269
+ return {};
270
+ }
271
+ }
272
+
273
+ const controller = new ResourceController(resource);
274
+ const plan = await controller.plan({ type: 'type', propA: ['3.11'] } as any)
275
+
276
+ expect(plan).toMatchObject({
277
+ changeSet: {
278
+ operation: ResourceOperation.NOOP,
279
+ }
280
+ })
281
+ })
282
+
283
+ it('Plans stateful parameters in the order specified', async () => {
284
+ const statefulParameterA = spy(new class extends TestStatefulParameter {
285
+ async refresh(): Promise<any | null> {
286
+ return performance.now()
287
+ }
288
+ });
289
+
290
+ const statefulParameterB = spy(new class extends TestStatefulParameter {
291
+ async refresh(): Promise<any | null> {
292
+ return performance.now()
293
+ }
294
+ });
295
+
296
+ const statefulParameterC = spy(new class extends TestStatefulParameter {
297
+ async refresh(): Promise<any | null> {
298
+ return performance.now()
299
+ }
300
+ });
301
+
302
+ const statefulParameterD = spy(new class extends TestStatefulParameter {
303
+ async refresh(): Promise<any | null> {
304
+ return performance.now()
305
+ }
306
+ });
307
+
308
+ const statefulParameterE = spy(new class extends TestStatefulParameter {
309
+ async refresh(): Promise<any | null> {
310
+ return performance.now()
311
+ }
312
+ });
313
+
314
+ const resource = spy(new class extends TestResource {
315
+ getSettings(): ResourceSettings<TestConfig> {
316
+ return {
317
+ id: 'resourceType',
318
+ parameterSettings: {
319
+ propA: { type: 'stateful', definition: statefulParameterA, order: 3 },
320
+ propB: { type: 'stateful', definition: statefulParameterB, order: 1 },
321
+ propC: { type: 'stateful', definition: statefulParameterC, order: 2 },
322
+ propD: { type: 'stateful', definition: statefulParameterD },
323
+ propE: { type: 'stateful', definition: statefulParameterE }
324
+ },
325
+ }
326
+ }
327
+
328
+ async refresh(): Promise<Partial<TestConfig> | null> {
329
+ return {};
330
+ }
331
+ });
332
+
333
+ const controller = new ResourceController(resource)
334
+ const plan = await controller.plan({
335
+ type: 'resourceType',
336
+ propA: 'propA',
337
+ propB: 10,
338
+ propC: 'propC',
339
+ propD: 'propD',
340
+ propE: 'propE',
341
+ });
342
+
343
+ expect(plan.currentConfig?.propB).to.be.lessThan(plan.currentConfig?.propC as any);
344
+ expect(plan.currentConfig?.propC).to.be.lessThan(plan.currentConfig?.propA as any);
345
+ expect(plan.currentConfig?.propA).to.be.lessThan(plan.currentConfig?.propD as any);
346
+ expect(plan.currentConfig?.propD).to.be.lessThan(plan.currentConfig?.propE as any);
347
+ })
348
+
349
+ it('Applies stateful parameters in the order specified', async () => {
350
+ let timestampA;
351
+ const statefulParameterA = spy(new class extends TestStatefulParameter {
352
+ add = async (): Promise<void> => {
353
+ timestampA = performance.now();
354
+ }
355
+ modify = async (): Promise<void> => {
356
+ timestampA = performance.now();
357
+ }
358
+ remove = async (): Promise<void> => {
359
+ timestampA = performance.now();
360
+ }
361
+ });
362
+
363
+ let timestampB
364
+ const statefulParameterB = spy(new class extends TestStatefulParameter {
365
+ add = async (): Promise<void> => {
366
+ timestampB = performance.now();
367
+ }
368
+ modify = async (): Promise<void> => {
369
+ timestampB = performance.now();
370
+ }
371
+ remove = async (): Promise<void> => {
372
+ timestampB = performance.now();
373
+ }
374
+ });
375
+
376
+ let timestampC
377
+ const statefulParameterC = spy(new class extends TestStatefulParameter {
378
+ add = async (): Promise<void> => {
379
+ timestampC = performance.now();
380
+ }
381
+ modify = async (): Promise<void> => {
382
+ timestampC = performance.now();
383
+ }
384
+ remove = async (): Promise<void> => {
385
+ timestampC = performance.now();
386
+ }
387
+ });
388
+
389
+ const resource = spy(new class extends TestResource {
390
+ getSettings(): ResourceSettings<TestConfig> {
391
+ return {
392
+ id: 'resourceType',
393
+ parameterSettings: {
394
+ propA: { type: 'stateful', definition: statefulParameterA, order: 3 },
395
+ propB: { type: 'stateful', definition: statefulParameterB, order: 1 },
396
+ propC: { type: 'stateful', definition: statefulParameterC, order: 2 },
397
+ },
398
+ removeStatefulParametersBeforeDestroy: true,
399
+ }
400
+ }
401
+ });
402
+
403
+ const controller = new ResourceController(resource);
404
+ await controller.apply(
405
+ Plan.fromResponse({
406
+ resourceType: 'resourceType',
407
+ operation: ResourceOperation.CREATE,
408
+ parameters: [
409
+ { name: 'propA', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
410
+ { name: 'propB', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
411
+ { name: 'propC', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
412
+ ]
413
+ }, {}) as any
414
+ );
415
+
416
+ if (!timestampB || !timestampC || !timestampA) {
417
+ throw new Error('Variable not initialized')
418
+ }
419
+
420
+ expect(timestampB).to.be.lessThan(timestampC as any);
421
+ expect(timestampC).to.be.lessThan(timestampA as any);
422
+ timestampA = 0;
423
+ timestampB = 0;
424
+ timestampC = 0;
425
+
426
+ await controller.apply(
427
+ Plan.fromResponse({
428
+ resourceType: 'resourceType',
429
+ operation: ResourceOperation.MODIFY,
430
+ parameters: [
431
+ { name: 'propA', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
432
+ { name: 'propB', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
433
+ { name: 'propC', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
434
+ ]
435
+ }, {}) as any
436
+ );
437
+
438
+ expect(timestampB).to.be.lessThan(timestampC as any);
439
+ expect(timestampC).to.be.lessThan(timestampA as any);
440
+ timestampA = 0;
441
+ timestampB = 0;
442
+ timestampC = 0;
443
+
444
+ await controller.apply(
445
+ Plan.fromResponse({
446
+ resourceType: 'resourceType',
447
+ operation: ResourceOperation.DESTROY,
448
+ parameters: [
449
+ { name: 'propA', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
450
+ { name: 'propB', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
451
+ { name: 'propC', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
452
+ ]
453
+ }, {}) as any
454
+ );
455
+
456
+ expect(timestampB).to.be.lessThan(timestampC as any);
457
+ expect(timestampC).to.be.lessThan(timestampA as any);
458
+ })
459
+
460
+ it('Supports transform parameters', async () => {
461
+ const resource = spy(new class extends TestResource {
462
+ getSettings(): ResourceSettings<TestConfig> {
463
+ return {
464
+ id: 'resourceType',
465
+ inputTransformation: (desired) => ({
466
+ propA: 'propA',
467
+ propB: 10,
468
+ })
469
+ }
470
+ }
471
+
472
+ async refresh(): Promise<Partial<TestConfig> | null> {
473
+ return {
474
+ propA: 'propA',
475
+ propB: 10,
476
+ }
477
+ }
478
+ });
479
+
480
+ const controller = new ResourceController(resource);
481
+ const plan = await controller.plan({ type: 'resourceType', propC: 'abc' } as any);
482
+
483
+ expect(resource.refresh.called).to.be.true;
484
+ expect(resource.refresh.getCall(0).firstArg['propA']).to.exist;
485
+ expect(resource.refresh.getCall(0).firstArg['propB']).to.exist;
486
+ expect(resource.refresh.getCall(0).firstArg['propC']).to.not.exist;
487
+
488
+ expect(plan.desiredConfig?.propA).to.eq('propA');
489
+ expect(plan.desiredConfig?.propB).to.eq(10);
490
+ expect(plan.desiredConfig?.propC).to.be.undefined;
491
+
492
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
493
+ })
494
+ })
@@ -0,0 +1,192 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+ import path from 'node:path';
3
+
4
+ import { untildify } from '../utils/utils.js';
5
+ import { StatefulParameter } from './stateful-parameter.js';
6
+
7
+ /**
8
+ * The configuration and settings for a resource.
9
+ */
10
+ export interface ResourceSettings<T extends StringIndexedObject> {
11
+
12
+ /**
13
+ * The typeId of the resource.
14
+ */
15
+ id: string;
16
+
17
+ /**
18
+ * Schema to validate user configs with. Must be in the format JSON Schema draft07
19
+ */
20
+ schema?: unknown;
21
+
22
+ /**
23
+ * Allow multiple of the same resource to unique. Set truthy if
24
+ * multiples are allowed, for example for applications, there can be multiple copy of the same application installed
25
+ * on the system. Or there can be multiple git repos. Defaults to false.
26
+ */
27
+ allowMultiple?: {
28
+
29
+ /**
30
+ * If multiple copies are allowed then a matcher must be defined to match the desired
31
+ * config with one of the resources currently existing on the system. Return null if there is no match.
32
+ *
33
+ * @param current An array of resources found installed on the system
34
+ * @param desired The desired config to match.
35
+ *
36
+ * @return The matched resource.
37
+ */
38
+ matcher: (desired: Partial<T>, current: Partial<T>[],) => Partial<T>
39
+ }
40
+
41
+ /**
42
+ * If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
43
+ * if the stateful parameter needs to be first uninstalled (cleanup) before the overall resource can be
44
+ * uninstalled. Defaults to false.
45
+ */
46
+ removeStatefulParametersBeforeDestroy?: boolean;
47
+
48
+ /**
49
+ * An array of type ids of resources that this resource depends on. This affects the order in which multiple resources are
50
+ * planned and applied.
51
+ */
52
+ dependencies?: string[];
53
+
54
+ /**
55
+ * Options for configuring parameters operations including overriding the equals function, adding default values
56
+ * and applying any input transformations. Use parameter settings to define stateful parameters as well.
57
+ */
58
+ parameterSettings?: Partial<Record<keyof T, ParameterSetting>>;
59
+
60
+ /**
61
+ * A config level transformation that is only applied to the user supplied desired config. This transformation is allowed
62
+ * to add, remove or modify keys as well as values. Changing this transformation for existing libraries will mess up existing states.
63
+ *
64
+ * @param desired
65
+ */
66
+ inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
67
+ }
68
+
69
+ /**
70
+ * The type of parameter. This value is mainly used to determine a pre-set equality method for comparing the current
71
+ * config with desired config. Certain types will have additional options to help support it. For example the type
72
+ * stateful requires a stateful parameter definition and type array takes an isElementEqual method.
73
+ */
74
+ export type ParameterSettingType =
75
+ 'any'
76
+ | 'array'
77
+ | 'boolean'
78
+ | 'directory'
79
+ | 'number'
80
+ | 'stateful'
81
+ | 'string'
82
+ | 'version';
83
+
84
+ /**
85
+ * Typing information for the parameter setting. This represents a setting on a specific parameter within a
86
+ * resource. Options for configuring parameters operations including overriding the equals function, adding default values
87
+ * and applying any input transformations. See {@link DefaultParameterSetting } for more information.
88
+ * Use parameter settings to define stateful parameters as well.
89
+ */
90
+
91
+ export type ParameterSetting =
92
+ ArrayParameterSetting
93
+ | DefaultParameterSetting
94
+ | StatefulParameterSetting
95
+
96
+ /**
97
+ * The parent class for parameter settings. The options are applicable to array parameter settings
98
+ * as well.
99
+ */
100
+ export interface DefaultParameterSetting {
101
+ /**
102
+ * The type of the value of this parameter. See {@link ParameterSettingType} for the available options. This value
103
+ * is mainly used to determine the equality method when performing diffing.
104
+ */
105
+ type?: ParameterSettingType;
106
+
107
+ /**
108
+ * Default value for the parameter. If a value is not provided in the config, then this value will be used.
109
+ */
110
+ default?: unknown;
111
+
112
+ /**
113
+ * A transformation of the input value for this parameter. This transformation is only applied to the desired parameter
114
+ * value supplied by the user.
115
+ *
116
+ * @param input The original parameter value from the desired config.
117
+ */
118
+ inputTransformation?: (input: any) => Promise<any> | unknown;
119
+
120
+ /**
121
+ * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
122
+ * This value will override the pre-set equality function from the type. Return true if the desired value is
123
+ * equivalent to the current value.
124
+ *
125
+ * @param desired The desired value.
126
+ * @param current The current value.
127
+ *
128
+ * @return Return true if equal
129
+ */
130
+ isEqual?: (desired: any, current: any) => boolean;
131
+
132
+ /**
133
+ * Chose if the resource can be modified instead of re-created when there is a change to this parameter.
134
+ * Defaults to false (re-create).
135
+ *
136
+ * Examples:
137
+ * 1. Settings like git user name and git user email that have setter calls and don't require the re-installation of git
138
+ * 2. AWS profile secret keys that can be updated without the re-installation of AWS CLI
139
+ */
140
+ canModify?: boolean
141
+ }
142
+
143
+ /**
144
+ * Array type specific settings. See {@link DefaultParameterSetting } for a full list of options.
145
+ */
146
+ export interface ArrayParameterSetting extends DefaultParameterSetting {
147
+ type: 'array'
148
+
149
+ /**
150
+ * An element level equality function for arrays. The diffing algorithm will take isElementEqual and use it in a
151
+ * O(n^2) equality comparison to determine if the overall array is equal. This value will override the pre-set equality
152
+ * function for arrays (desired === current). Return true if the desired element is equivalent to the current element.
153
+ *
154
+ * @param desired An element of the desired array
155
+ * @param current An element of the current array
156
+ *
157
+ * @return Return true if desired is equivalent to current.
158
+ */
159
+ isElementEqual?: (desired: any, current: any) => boolean
160
+ }
161
+
162
+ /**
163
+ * Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
164
+ * state but is still tied to the overall state of the resource. For example 'homebrew' is represented
165
+ * as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
166
+ * modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
167
+ *
168
+ */
169
+ export interface StatefulParameterSetting extends DefaultParameterSetting {
170
+ type: 'stateful',
171
+
172
+ /**
173
+ * The stateful parameter definition. A stateful parameter is a sub-resource that can hold its own
174
+ * state but is still tied to the overall state of the resource. For example 'homebrew' is represented
175
+ * as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
176
+ * modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
177
+ */
178
+ definition: StatefulParameter<any, unknown>,
179
+
180
+ /**
181
+ * The order multiple stateful parameters should be applied in. The order is applied in ascending order (1, 2, 3...).
182
+ */
183
+ order?: number,
184
+ }
185
+
186
+ export const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
187
+ 'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
188
+ 'directory': (a: unknown, b: unknown) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
189
+ 'number': (a: unknown, b: unknown) => Number(a) === Number(b),
190
+ 'string': (a: unknown, b: unknown) => String(a) === String(b),
191
+ 'version': (desired: unknown, current: unknown) => String(current).includes(String(desired))
192
+ }