@codifycli/plugin-core 1.0.0-beta1

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 (152) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +30 -0
  3. package/.github/workflows/release.yaml +19 -0
  4. package/.github/workflows/unit-test-ci.yaml +18 -0
  5. package/.prettierrc.json +1 -0
  6. package/bin/build.js +189 -0
  7. package/dist/bin/build.d.ts +1 -0
  8. package/dist/bin/build.js +80 -0
  9. package/dist/bin/deploy-plugin.d.ts +2 -0
  10. package/dist/bin/deploy-plugin.js +8 -0
  11. package/dist/common/errors.d.ts +8 -0
  12. package/dist/common/errors.js +24 -0
  13. package/dist/entities/change-set.d.ts +24 -0
  14. package/dist/entities/change-set.js +152 -0
  15. package/dist/entities/errors.d.ts +4 -0
  16. package/dist/entities/errors.js +7 -0
  17. package/dist/entities/plan-types.d.ts +25 -0
  18. package/dist/entities/plan-types.js +1 -0
  19. package/dist/entities/plan.d.ts +15 -0
  20. package/dist/entities/plan.js +127 -0
  21. package/dist/entities/plugin.d.ts +16 -0
  22. package/dist/entities/plugin.js +80 -0
  23. package/dist/entities/resource-options.d.ts +31 -0
  24. package/dist/entities/resource-options.js +76 -0
  25. package/dist/entities/resource-types.d.ts +11 -0
  26. package/dist/entities/resource-types.js +1 -0
  27. package/dist/entities/resource.d.ts +42 -0
  28. package/dist/entities/resource.js +303 -0
  29. package/dist/entities/stateful-parameter.d.ts +29 -0
  30. package/dist/entities/stateful-parameter.js +46 -0
  31. package/dist/entities/transform-parameter.d.ts +4 -0
  32. package/dist/entities/transform-parameter.js +2 -0
  33. package/dist/errors.d.ts +4 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/index.d.ts +20 -0
  36. package/dist/index.js +26 -0
  37. package/dist/messages/handlers.d.ts +14 -0
  38. package/dist/messages/handlers.js +134 -0
  39. package/dist/messages/sender.d.ts +11 -0
  40. package/dist/messages/sender.js +57 -0
  41. package/dist/plan/change-set.d.ts +53 -0
  42. package/dist/plan/change-set.js +153 -0
  43. package/dist/plan/plan-types.d.ts +23 -0
  44. package/dist/plan/plan-types.js +1 -0
  45. package/dist/plan/plan.d.ts +66 -0
  46. package/dist/plan/plan.js +328 -0
  47. package/dist/plugin/plugin.d.ts +24 -0
  48. package/dist/plugin/plugin.js +200 -0
  49. package/dist/pty/background-pty.d.ts +21 -0
  50. package/dist/pty/background-pty.js +127 -0
  51. package/dist/pty/index.d.ts +50 -0
  52. package/dist/pty/index.js +20 -0
  53. package/dist/pty/promise-queue.d.ts +5 -0
  54. package/dist/pty/promise-queue.js +26 -0
  55. package/dist/pty/seqeuntial-pty.d.ts +17 -0
  56. package/dist/pty/seqeuntial-pty.js +119 -0
  57. package/dist/pty/vitest.config.d.ts +2 -0
  58. package/dist/pty/vitest.config.js +11 -0
  59. package/dist/resource/config-parser.d.ts +11 -0
  60. package/dist/resource/config-parser.js +21 -0
  61. package/dist/resource/parsed-resource-settings.d.ts +47 -0
  62. package/dist/resource/parsed-resource-settings.js +196 -0
  63. package/dist/resource/resource-controller.d.ts +36 -0
  64. package/dist/resource/resource-controller.js +402 -0
  65. package/dist/resource/resource-settings.d.ts +303 -0
  66. package/dist/resource/resource-settings.js +147 -0
  67. package/dist/resource/resource.d.ts +144 -0
  68. package/dist/resource/resource.js +44 -0
  69. package/dist/resource/stateful-parameter.d.ts +165 -0
  70. package/dist/resource/stateful-parameter.js +94 -0
  71. package/dist/scripts/deploy.d.ts +1 -0
  72. package/dist/scripts/deploy.js +2 -0
  73. package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
  74. package/dist/stateful-parameter/stateful-parameter-controller.js +81 -0
  75. package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
  76. package/dist/stateful-parameter/stateful-parameter.js +43 -0
  77. package/dist/test.d.ts +1 -0
  78. package/dist/test.js +5 -0
  79. package/dist/utils/codify-spawn.d.ts +29 -0
  80. package/dist/utils/codify-spawn.js +136 -0
  81. package/dist/utils/debug.d.ts +2 -0
  82. package/dist/utils/debug.js +10 -0
  83. package/dist/utils/file-utils.d.ts +23 -0
  84. package/dist/utils/file-utils.js +186 -0
  85. package/dist/utils/functions.d.ts +12 -0
  86. package/dist/utils/functions.js +74 -0
  87. package/dist/utils/index.d.ts +46 -0
  88. package/dist/utils/index.js +271 -0
  89. package/dist/utils/internal-utils.d.ts +12 -0
  90. package/dist/utils/internal-utils.js +74 -0
  91. package/dist/utils/load-resources.d.ts +1 -0
  92. package/dist/utils/load-resources.js +46 -0
  93. package/dist/utils/package-json-utils.d.ts +12 -0
  94. package/dist/utils/package-json-utils.js +34 -0
  95. package/dist/utils/pty-local-storage.d.ts +2 -0
  96. package/dist/utils/pty-local-storage.js +2 -0
  97. package/dist/utils/spawn-2.d.ts +5 -0
  98. package/dist/utils/spawn-2.js +7 -0
  99. package/dist/utils/spawn.d.ts +29 -0
  100. package/dist/utils/spawn.js +124 -0
  101. package/dist/utils/utils.d.ts +18 -0
  102. package/dist/utils/utils.js +86 -0
  103. package/dist/utils/verbosity-level.d.ts +5 -0
  104. package/dist/utils/verbosity-level.js +9 -0
  105. package/package.json +59 -0
  106. package/rollup.config.js +24 -0
  107. package/src/common/errors.test.ts +43 -0
  108. package/src/common/errors.ts +31 -0
  109. package/src/errors.ts +8 -0
  110. package/src/index.test.ts +6 -0
  111. package/src/index.ts +30 -0
  112. package/src/messages/handlers.test.ts +329 -0
  113. package/src/messages/handlers.ts +181 -0
  114. package/src/messages/sender.ts +69 -0
  115. package/src/plan/change-set.test.ts +280 -0
  116. package/src/plan/change-set.ts +236 -0
  117. package/src/plan/plan-types.ts +27 -0
  118. package/src/plan/plan.test.ts +413 -0
  119. package/src/plan/plan.ts +499 -0
  120. package/src/plugin/plugin.test.ts +533 -0
  121. package/src/plugin/plugin.ts +291 -0
  122. package/src/pty/background-pty.test.ts +69 -0
  123. package/src/pty/background-pty.ts +154 -0
  124. package/src/pty/index.test.ts +129 -0
  125. package/src/pty/index.ts +66 -0
  126. package/src/pty/promise-queue.ts +33 -0
  127. package/src/pty/seqeuntial-pty.ts +151 -0
  128. package/src/pty/sequential-pty.test.ts +194 -0
  129. package/src/resource/config-parser.ts +42 -0
  130. package/src/resource/parsed-resource-settings.test.ts +186 -0
  131. package/src/resource/parsed-resource-settings.ts +307 -0
  132. package/src/resource/resource-controller-stateful-mode.test.ts +253 -0
  133. package/src/resource/resource-controller.test.ts +1081 -0
  134. package/src/resource/resource-controller.ts +563 -0
  135. package/src/resource/resource-settings.test.ts +1213 -0
  136. package/src/resource/resource-settings.ts +545 -0
  137. package/src/resource/resource.ts +157 -0
  138. package/src/stateful-parameter/stateful-parameter-controller.test.ts +244 -0
  139. package/src/stateful-parameter/stateful-parameter-controller.ts +111 -0
  140. package/src/stateful-parameter/stateful-parameter.ts +160 -0
  141. package/src/utils/debug.ts +11 -0
  142. package/src/utils/file-utils.test.ts +7 -0
  143. package/src/utils/file-utils.ts +231 -0
  144. package/src/utils/functions.ts +103 -0
  145. package/src/utils/index.ts +340 -0
  146. package/src/utils/internal-utils.test.ts +52 -0
  147. package/src/utils/pty-local-storage.ts +3 -0
  148. package/src/utils/test-utils.test.ts +96 -0
  149. package/src/utils/verbosity-level.ts +11 -0
  150. package/tsconfig.json +26 -0
  151. package/tsconfig.test.json +9 -0
  152. package/vitest.config.ts +10 -0
@@ -0,0 +1,563 @@
1
+ import { Ajv, ValidateFunction } from 'ajv';
2
+ import cleanDeep from 'clean-deep';
3
+ import {
4
+ ParameterOperation,
5
+ ResourceConfig,
6
+ ResourceJson,
7
+ ResourceOperation,
8
+ StringIndexedObject,
9
+ ValidateResponseData
10
+ } from 'codify-schemas';
11
+
12
+ import { ParameterChange } from '../plan/change-set.js';
13
+ import { Plan } from '../plan/plan.js';
14
+ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
15
+ import { ConfigParser } from './config-parser.js';
16
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
17
+ import { RefreshContext, Resource } from './resource.js';
18
+ import { ResourceSettings } from './resource-settings.js';
19
+
20
+ export class ResourceController<T extends StringIndexedObject> {
21
+ readonly resource: Resource<T>
22
+ readonly settings: ResourceSettings<T>
23
+ readonly parsedSettings: ParsedResourceSettings<T>
24
+
25
+ readonly typeId: string;
26
+ readonly dependencies: string[];
27
+
28
+ protected ajv?: Ajv;
29
+ protected schemaValidator?: ValidateFunction;
30
+
31
+ constructor(
32
+ resource: Resource<T>,
33
+ ) {
34
+ this.resource = resource;
35
+ this.settings = resource.getSettings();
36
+
37
+ this.typeId = this.settings.id;
38
+ this.dependencies = this.settings.dependencies ?? [];
39
+ this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
40
+
41
+ if (this.parsedSettings.schema) {
42
+ this.ajv = new Ajv({
43
+ allErrors: true,
44
+ strict: true,
45
+ strictRequired: false,
46
+ allowUnionTypes: true
47
+ })
48
+ this.schemaValidator = this.ajv.compile(this.parsedSettings.schema);
49
+ }
50
+ }
51
+
52
+ async initialize(): Promise<void> {
53
+ return this.resource.initialize();
54
+ }
55
+
56
+ async validate(
57
+ core: ResourceConfig,
58
+ parameters: Partial<T>,
59
+ ): Promise<ValidateResponseData['resourceValidations'][0]> {
60
+ const originalParameters = structuredClone(parameters);
61
+ await this.applyTransformations(parameters, undefined, true);
62
+ this.addDefaultValues(parameters);
63
+
64
+ if (this.schemaValidator) {
65
+ // Schema validator uses pre transformation parameters
66
+ const isValid = this.schemaValidator(
67
+ // @ts-expect-error Non esm package
68
+ cleanDeep(originalParameters, {
69
+ nullValues: true,
70
+ undefinedValues: true,
71
+ emptyArrays: false,
72
+ emptyStrings: false,
73
+ emptyObjects: false,
74
+ NaNValues: false
75
+ })
76
+ );
77
+
78
+ if (!isValid) {
79
+ return {
80
+ isValid: false,
81
+ resourceName: core.name,
82
+ resourceType: core.type,
83
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
84
+ }
85
+ }
86
+ }
87
+
88
+ let isValid = true;
89
+ let customValidationErrorMessage;
90
+ try {
91
+ await this.resource.validate(parameters);
92
+ } catch (error) {
93
+ isValid = false;
94
+ customValidationErrorMessage = (error as Error).message;
95
+ }
96
+
97
+ if (!isValid) {
98
+ return {
99
+ customValidationErrorMessage,
100
+ isValid: false,
101
+ resourceName: core.name,
102
+ resourceType: core.type,
103
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
104
+ }
105
+ }
106
+
107
+ return {
108
+ isValid: true,
109
+ resourceName: core.name,
110
+ resourceType: core.type,
111
+ schemaValidationErrors: [],
112
+ }
113
+ }
114
+
115
+ async match(resource: ResourceJson, array: Array<ResourceJson>): Promise<ResourceJson | undefined> {
116
+ if (resource.core.type !== this.typeId) {
117
+ throw new Error(`Unknown type passed into match method: ${resource.core.type} for ${this.typeId}`);
118
+ }
119
+
120
+ if (!this.parsedSettings.allowMultiple) {
121
+ return array.find((r) => r.core.type === resource.core.type)
122
+ }
123
+
124
+
125
+ const { name, type } = resource.core;
126
+ const parameterMatcher = this.parsedSettings.matcher;
127
+
128
+ for (const resourceToMatch of array) {
129
+ if (type !== resourceToMatch.core.type) {
130
+ return undefined;
131
+ }
132
+
133
+ // If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
134
+ if (name === resourceToMatch.core.name
135
+ && name
136
+ && Number.isInteger(Number.parseInt(name, 10))
137
+ ) {
138
+ return resourceToMatch;
139
+ }
140
+
141
+ const originalParams = structuredClone(resource.parameters) as Partial<T>;
142
+ const paramsToMatch = structuredClone(resourceToMatch.parameters) as Partial<T>;
143
+
144
+ this.addDefaultValues(originalParams);
145
+ await this.applyTransformations(originalParams);
146
+
147
+ this.addDefaultValues(paramsToMatch);
148
+ await this.applyTransformations(paramsToMatch);
149
+
150
+ const match = parameterMatcher(originalParams, paramsToMatch);
151
+ if (match) {
152
+ return resourceToMatch;
153
+ }
154
+ }
155
+ }
156
+
157
+ async plan(
158
+ core: ResourceConfig,
159
+ desired: Partial<T> | null,
160
+ state: Partial<T> | null,
161
+ isStateful = false,
162
+ commandType = 'plan',
163
+ ): Promise<Plan<T>> {
164
+ this.validatePlanInputs(core, desired, state, isStateful);
165
+ const context: RefreshContext<T> = {
166
+ commandType: commandType as 'plan' | 'validationPlan',
167
+ isStateful,
168
+ originalDesiredConfig: structuredClone(desired),
169
+ };
170
+
171
+ this.addDefaultValues(desired);
172
+ await this.applyTransformations(desired);
173
+
174
+ this.addDefaultValues(state);
175
+ await this.applyTransformations(state);
176
+
177
+ // Parse data from the user supplied config
178
+ const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters)
179
+ const {
180
+ allParameters,
181
+ allNonStatefulParameters,
182
+ allStatefulParameters,
183
+ } = parsedConfig;
184
+
185
+ // Refresh resource parameters. This refreshes the parameters that configure the resource itself
186
+ const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
187
+
188
+ // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
189
+ if (currentArray === null
190
+ || currentArray === undefined
191
+ || currentArray.length === 0
192
+ || currentArray.filter(Boolean).length === 0
193
+ ) {
194
+ return Plan.calculate({
195
+ desired,
196
+ currentArray,
197
+ state,
198
+ core,
199
+ settings: this.parsedSettings,
200
+ isStateful,
201
+ });
202
+ }
203
+
204
+ // Refresh stateful parameters. These parameters have state external to the resource. Each variation of the
205
+ // current parameters (each array element) is passed into the stateful parameter refresh.
206
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray, allParameters);
207
+
208
+ return Plan.calculate({
209
+ desired,
210
+ currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })),
211
+ state,
212
+ core,
213
+ settings: this.parsedSettings,
214
+ isStateful
215
+ })
216
+ }
217
+
218
+ async planDestroy(
219
+ core: ResourceConfig,
220
+ parameters: Partial<T>
221
+ ): Promise<Plan<T>> {
222
+ this.addDefaultValues(parameters);
223
+ await this.applyTransformations(parameters);
224
+
225
+ // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
226
+ const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
227
+ ? {
228
+ ...Object.fromEntries(
229
+ this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])
230
+ ),
231
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
232
+ ...parameters,
233
+ }
234
+ : {
235
+ ...Object.fromEntries(
236
+ this.getAllParameterKeys().map((k) => [k, null])
237
+ ),
238
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
239
+ ...parameters,
240
+ };
241
+
242
+ return this.plan(core, null, parametersToRefresh, true);
243
+ }
244
+
245
+ async apply(plan: Plan<T>): Promise<void> {
246
+ if (plan.getResourceType() !== this.typeId) {
247
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
248
+ }
249
+
250
+ switch (plan.changeSet.operation) {
251
+ case ResourceOperation.CREATE: {
252
+ return this.applyCreate(plan);
253
+ }
254
+
255
+ case ResourceOperation.MODIFY: {
256
+ return this.applyModify(plan);
257
+ }
258
+
259
+ case ResourceOperation.RECREATE: {
260
+ await this.applyDestroy(plan);
261
+ return this.applyCreate(plan);
262
+ }
263
+
264
+ case ResourceOperation.DESTROY: {
265
+ return this.applyDestroy(plan);
266
+ }
267
+ }
268
+ }
269
+
270
+ async import(
271
+ core: ResourceConfig,
272
+ parameters: Partial<T>,
273
+ autoSearchAll = false,
274
+ ): Promise<Array<ResourceJson> | null> {
275
+ if (this.settings.importAndDestroy?.preventImport) {
276
+ throw new Error(`Type: ${this.typeId} cannot be imported`);
277
+ }
278
+
279
+ const context: RefreshContext<T> = {
280
+ commandType: 'import',
281
+ isStateful: true,
282
+ originalDesiredConfig: structuredClone(parameters),
283
+ };
284
+
285
+ // Auto search means that no required parameters will be provided. We will try to generate it ourselves or return an
286
+ // empty array if they can't be.
287
+ if (autoSearchAll && this.settings.allowMultiple) {
288
+ if (this.settings.allowMultiple === true || !this.settings.allowMultiple.findAllParameters?.()) {
289
+ return [];
290
+ }
291
+
292
+ const parametersToImport = await this.settings.allowMultiple.findAllParameters?.();
293
+ const results = await Promise.all(parametersToImport.map((p) =>
294
+ this.import(core, p).catch(() => null))
295
+ );
296
+ return results.filter(Boolean).flat() as ResourceJson[];
297
+ }
298
+
299
+ this.addDefaultValues(parameters);
300
+ await this.applyTransformations(parameters);
301
+
302
+ // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
303
+ const parametersToRefresh = this.getParametersToRefreshForImport(parameters, context);
304
+
305
+ // Parse data from the user supplied config
306
+ const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters)
307
+ const {
308
+ allParameters,
309
+ allNonStatefulParameters,
310
+ allStatefulParameters,
311
+ } = parsedConfig;
312
+
313
+ const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
314
+
315
+ if (currentParametersArray === null
316
+ || currentParametersArray === undefined
317
+ || currentParametersArray.filter(Boolean).length === 0
318
+ ) {
319
+ return [];
320
+ }
321
+
322
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray, allParameters);
323
+ const resultParametersArray = currentParametersArray
324
+ ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] }))
325
+
326
+ for (const result of resultParametersArray) {
327
+ await this.applyTransformations(result, { original: context.originalDesiredConfig });
328
+ this.removeDefaultValues(result, parameters);
329
+ }
330
+
331
+ return resultParametersArray?.map((r) => ({ core, parameters: r }))
332
+ }
333
+
334
+ private async applyCreate(plan: Plan<T>): Promise<void> {
335
+ await this.resource.create(plan as CreatePlan<T>);
336
+
337
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
338
+
339
+ for (const parameterChange of statefulParameterChanges) {
340
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
341
+ await statefulParameter.add(parameterChange.newValue, plan);
342
+ }
343
+ }
344
+
345
+ private async applyModify(plan: Plan<T>): Promise<void> {
346
+ const parameterChanges = plan
347
+ .changeSet
348
+ .parameterChanges
349
+ .filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
350
+
351
+ const statelessParameterChanges = parameterChanges
352
+ .filter((pc: ParameterChange<T>) => !this.parsedSettings.statefulParameters.has(pc.name))
353
+
354
+ for (const pc of statelessParameterChanges) {
355
+ await this.resource.modify(pc, plan as ModifyPlan<T>);
356
+ }
357
+
358
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
359
+
360
+ for (const parameterChange of statefulParameterChanges) {
361
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
362
+
363
+ switch (parameterChange.operation) {
364
+ case ParameterOperation.ADD: {
365
+ await statefulParameter.add(parameterChange.newValue, plan);
366
+ break;
367
+ }
368
+
369
+ case ParameterOperation.MODIFY: {
370
+ await statefulParameter.modify(parameterChange.newValue, parameterChange.previousValue, plan);
371
+ break;
372
+ }
373
+
374
+ case ParameterOperation.REMOVE: {
375
+ await statefulParameter.remove(parameterChange.previousValue, plan);
376
+ break;
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ private async applyDestroy(plan: Plan<T>): Promise<void> {
383
+ // If this option is set (defaults to false), then stateful parameters need to be destroyed
384
+ // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
385
+ if (this.settings.removeStatefulParametersBeforeDestroy) {
386
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
387
+
388
+ for (const parameterChange of statefulParameterChanges) {
389
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
390
+ await statefulParameter.remove(parameterChange.previousValue, plan);
391
+ }
392
+ }
393
+
394
+ await this.resource.destroy(plan as DestroyPlan<T>);
395
+ }
396
+
397
+ private validateRefreshResults(refresh: Array<Partial<T>> | null) {
398
+ if (!refresh) {
399
+ return;
400
+ }
401
+
402
+ if (!this.settings.allowMultiple && refresh.length > 1) {
403
+ throw new Error(`Resource: ${this.settings.id}. Allow multiple was set to false but multiple refresh results were returned.
404
+
405
+ ${JSON.stringify(refresh, null, 2)}
406
+ `)
407
+ }
408
+ }
409
+
410
+ private async applyTransformations(config: Partial<T> | null, reverse?: {
411
+ original: Partial<T> | null
412
+ }, skipConfigTransformation = false): Promise<void> {
413
+ if (!config) {
414
+ return;
415
+ }
416
+
417
+ for (const [key, inputTransformation] of Object.entries(this.parsedSettings.inputTransformations)) {
418
+ if (config[key] === undefined || !inputTransformation) {
419
+ continue;
420
+ }
421
+
422
+ (config as Record<string, unknown>)[key] = reverse
423
+ ? await inputTransformation.from(config[key], reverse.original?.[key])
424
+ : await inputTransformation.to(config[key]);
425
+ }
426
+
427
+ if (this.settings.transformation && !skipConfigTransformation) {
428
+ const transformed = reverse
429
+ ? await this.settings.transformation.from({ ...config }, reverse.original)
430
+ : await this.settings.transformation.to({ ...config })
431
+
432
+ Object.keys(config).forEach((k) => delete config[k])
433
+ Object.assign(config, transformed);
434
+ }
435
+ }
436
+
437
+ private addDefaultValues(config: Partial<T> | null): void {
438
+ if (!config) {
439
+ return;
440
+ }
441
+
442
+ for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
443
+ if (defaultValue !== undefined && (config[key] === undefined || config[key] === null)) {
444
+ (config as Record<string, unknown>)[key] = defaultValue;
445
+ }
446
+ }
447
+ }
448
+
449
+ private removeDefaultValues(newConfig: Partial<T> | null, originalConfig: Partial<T>): void {
450
+ if (!newConfig) {
451
+ return;
452
+ }
453
+
454
+ for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
455
+ if (defaultValue !== undefined && (newConfig[key] === defaultValue || originalConfig[key] === undefined || originalConfig[key] === null)) {
456
+ delete newConfig[key];
457
+ }
458
+ }
459
+
460
+ }
461
+
462
+ private async refreshNonStatefulParameters(resourceParameters: Partial<T>, context: RefreshContext<T>): Promise<Array<Partial<T>> | null> {
463
+ const result = await this.resource.refresh(resourceParameters, context);
464
+
465
+ const currentParametersArray = Array.isArray(result) || result === null
466
+ ? result
467
+ : [result]
468
+
469
+ this.validateRefreshResults(currentParametersArray);
470
+ return currentParametersArray;
471
+ }
472
+
473
+ // Refresh stateful parameters
474
+ // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
475
+ private async refreshStatefulParameters(
476
+ statefulParametersConfig: Partial<T>,
477
+ currentArray: Array<Partial<T>>,
478
+ allParameters: Partial<T>
479
+ ): Promise<Array<Partial<T>>> {
480
+ const result: Array<Partial<T>> = Array.from({ length: currentArray.length }, () => ({}))
481
+ const sortedEntries = Object.entries(statefulParametersConfig)
482
+ .sort(
483
+ ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
484
+ )
485
+
486
+ for (const [idx, refreshedParams] of currentArray.entries()) {
487
+ await Promise.all(sortedEntries.map(async ([key, desiredValue]) => {
488
+ const statefulParameter = this.parsedSettings.statefulParameters.get(key);
489
+ if (!statefulParameter) {
490
+ throw new Error(`Stateful parameter ${key} was not found`);
491
+ }
492
+
493
+ (result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, { ...allParameters, ...refreshedParams })
494
+ }))
495
+ }
496
+
497
+ return result;
498
+ }
499
+
500
+ private validatePlanInputs(
501
+ core: ResourceConfig,
502
+ desired: Partial<T> | null,
503
+ current: Partial<T> | null,
504
+ isStateful: boolean,
505
+ ) {
506
+ if (!core || !core.type) {
507
+ throw new Error('Core parameters type must be defined');
508
+ }
509
+
510
+ if (!desired && !current) {
511
+ throw new Error('Desired config and current config cannot both be missing')
512
+ }
513
+
514
+ if (!isStateful && !desired) {
515
+ throw new Error('Desired config must be provided in non-stateful mode')
516
+ }
517
+ }
518
+
519
+ private getSortedStatefulParameterChanges(parameterChanges: ParameterChange<T>[]) {
520
+ return parameterChanges
521
+ .filter((pc: ParameterChange<T>) => this.parsedSettings.statefulParameters.has(pc.name))
522
+ .sort((a, b) =>
523
+ this.parsedSettings.statefulParameterOrder.get(a.name)! - this.parsedSettings.statefulParameterOrder.get(b.name)!
524
+ )
525
+ }
526
+
527
+ private getAllParameterKeys(): string[] {
528
+ return this.parsedSettings.schema
529
+ ? Object.keys((this.parsedSettings.schema as any)?.properties)
530
+ : Object.keys(this.parsedSettings.parameterSettings);
531
+ }
532
+
533
+ private getParametersToRefreshForImport(parameters: Partial<T>, context: RefreshContext<T>): Partial<T> {
534
+ if (this.settings.importAndDestroy?.refreshMapper) {
535
+ return this.settings.importAndDestroy?.refreshMapper(parameters, context);
536
+ }
537
+
538
+ return this.settings.importAndDestroy?.refreshKeys
539
+ ? {
540
+ ...Object.fromEntries(
541
+ this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])
542
+ ),
543
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
544
+ ...parameters,
545
+ ...(Object.fromEntries( // If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead
546
+ Object.entries(parameters).filter(([k, v]) =>
547
+ this.parsedSettings.defaultValues[k] !== undefined
548
+ && v === this.parsedSettings.defaultValues[k]
549
+ && context.originalDesiredConfig?.[k] === undefined
550
+ && this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined
551
+ ).map(([k]) => [k, this.settings.importAndDestroy!.defaultRefreshValues![k]])
552
+ ))
553
+ }
554
+ : {
555
+ ...Object.fromEntries(
556
+ this.getAllParameterKeys().map((k) => [k, null])
557
+ ),
558
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
559
+ ...parameters,
560
+ };
561
+ }
562
+ }
563
+