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,340 @@
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 { ParameterChange } from '../plan/change-set.js';
11
+ import { Plan } from '../plan/plan.js';
12
+ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
13
+ import { ConfigParser } from './config-parser.js';
14
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
15
+ import { Resource } from './resource.js';
16
+ import { ResourceSettings } from './resource-settings.js';
17
+
18
+ export class ResourceController<T extends StringIndexedObject> {
19
+ readonly resource: Resource<T>
20
+ readonly settings: ResourceSettings<T>
21
+ readonly parsedSettings: ParsedResourceSettings<T>
22
+
23
+ readonly typeId: string;
24
+ readonly dependencies: string[];
25
+
26
+ protected ajv?: Ajv;
27
+ protected schemaValidator?: ValidateFunction;
28
+
29
+ constructor(
30
+ resource: Resource<T>,
31
+ ) {
32
+ this.resource = resource;
33
+ this.settings = resource.getSettings();
34
+
35
+ this.typeId = this.settings.id;
36
+ this.dependencies = this.settings.dependencies ?? [];
37
+
38
+ if (this.settings.schema) {
39
+ this.ajv = new Ajv({
40
+ allErrors: true,
41
+ strict: true,
42
+ strictRequired: false,
43
+ })
44
+ this.schemaValidator = this.ajv.compile(this.settings.schema);
45
+ }
46
+
47
+ this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
48
+ }
49
+
50
+ async initialize(): Promise<void> {
51
+ return this.resource.initialize();
52
+ }
53
+
54
+ async validate(
55
+ parameters: Partial<T>,
56
+ resourceMetaData: ResourceConfig
57
+ ): Promise<ValidateResponseData['resourceValidations'][0]> {
58
+ if (this.schemaValidator) {
59
+ const isValid = this.schemaValidator(parameters);
60
+
61
+ if (!isValid) {
62
+ return {
63
+ isValid: false,
64
+ resourceName: resourceMetaData.name,
65
+ resourceType: resourceMetaData.type,
66
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
67
+ }
68
+ }
69
+ }
70
+
71
+ let isValid = true;
72
+ let customValidationErrorMessage;
73
+ try {
74
+ await this.resource.validate(parameters);
75
+ } catch (error) {
76
+ isValid = false;
77
+ customValidationErrorMessage = (error as Error).message;
78
+ }
79
+
80
+ if (!isValid) {
81
+ return {
82
+ customValidationErrorMessage,
83
+ isValid: false,
84
+ resourceName: resourceMetaData.name,
85
+ resourceType: resourceMetaData.type,
86
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
87
+ }
88
+ }
89
+
90
+ return {
91
+ isValid: true,
92
+ resourceName: resourceMetaData.name,
93
+ resourceType: resourceMetaData.type,
94
+ schemaValidationErrors: [],
95
+ }
96
+ }
97
+
98
+ async plan(
99
+ desiredConfig: Partial<T> & ResourceConfig | null,
100
+ stateConfig: Partial<T> & ResourceConfig | null = null,
101
+ statefulMode = false,
102
+ ): Promise<Plan<T>> {
103
+ this.validatePlanInputs(desiredConfig, stateConfig, statefulMode);
104
+
105
+ this.addDefaultValues(desiredConfig);
106
+ await this.applyTransformParameters(desiredConfig);
107
+
108
+ // Parse data from the user supplied config
109
+ const parsedConfig = new ConfigParser(desiredConfig, stateConfig, this.parsedSettings.statefulParameters)
110
+ const {
111
+ coreParameters,
112
+ desiredParameters,
113
+ stateParameters,
114
+ allNonStatefulParameters,
115
+ allStatefulParameters,
116
+ } = parsedConfig;
117
+
118
+ // Refresh resource parameters. This refreshes the parameters that configure the resource itself
119
+ const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
120
+
121
+ // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
122
+ if (currentParametersArray === null
123
+ || currentParametersArray === undefined
124
+ || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
125
+ || currentParametersArray.length === 0
126
+ || currentParametersArray.filter(Boolean).length === 0
127
+ ) {
128
+ return Plan.calculate({
129
+ desiredParameters,
130
+ currentParametersArray,
131
+ stateParameters,
132
+ coreParameters,
133
+ settings: this.parsedSettings,
134
+ statefulMode,
135
+ });
136
+ }
137
+
138
+ // Refresh stateful parameters. These parameters have state external to the resource. allowMultiple
139
+ // does not work together with stateful parameters
140
+ const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters);
141
+
142
+ return Plan.calculate({
143
+ desiredParameters,
144
+ currentParametersArray: [{ ...currentParametersArray[0], ...statefulCurrentParameters }] as Partial<T>[],
145
+ stateParameters,
146
+ coreParameters,
147
+ settings: this.parsedSettings,
148
+ statefulMode
149
+ })
150
+ }
151
+
152
+ async apply(plan: Plan<T>): Promise<void> {
153
+ if (plan.getResourceType() !== this.typeId) {
154
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
155
+ }
156
+
157
+ switch (plan.changeSet.operation) {
158
+ case ResourceOperation.CREATE: {
159
+ return this.applyCreate(plan);
160
+ }
161
+
162
+ case ResourceOperation.MODIFY: {
163
+ return this.applyModify(plan);
164
+ }
165
+
166
+ case ResourceOperation.RECREATE: {
167
+ await this.applyDestroy(plan);
168
+ return this.applyCreate(plan);
169
+ }
170
+
171
+ case ResourceOperation.DESTROY: {
172
+ return this.applyDestroy(plan);
173
+ }
174
+ }
175
+ }
176
+
177
+ private async applyCreate(plan: Plan<T>): Promise<void> {
178
+ await this.resource.create(plan as CreatePlan<T>);
179
+
180
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
181
+
182
+ for (const parameterChange of statefulParameterChanges) {
183
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
184
+ await statefulParameter.add(parameterChange.newValue, plan);
185
+ }
186
+ }
187
+
188
+ private async applyModify(plan: Plan<T>): Promise<void> {
189
+ const parameterChanges = plan
190
+ .changeSet
191
+ .parameterChanges
192
+ .filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
193
+
194
+ const statelessParameterChanges = parameterChanges
195
+ .filter((pc: ParameterChange<T>) => !this.parsedSettings.statefulParameters.has(pc.name))
196
+
197
+ for (const pc of statelessParameterChanges) {
198
+ await this.resource.modify(pc, plan as ModifyPlan<T>);
199
+ }
200
+
201
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
202
+
203
+ for (const parameterChange of statefulParameterChanges) {
204
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
205
+
206
+ switch (parameterChange.operation) {
207
+ case ParameterOperation.ADD: {
208
+ await statefulParameter.add(parameterChange.newValue, plan);
209
+ break;
210
+ }
211
+
212
+ case ParameterOperation.MODIFY: {
213
+ await statefulParameter.modify(parameterChange.newValue, parameterChange.previousValue, plan);
214
+ break;
215
+ }
216
+
217
+ case ParameterOperation.REMOVE: {
218
+ await statefulParameter.remove(parameterChange.previousValue, plan);
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ private async applyDestroy(plan: Plan<T>): Promise<void> {
226
+ // If this option is set (defaults to false), then stateful parameters need to be destroyed
227
+ // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
228
+ if (this.settings.removeStatefulParametersBeforeDestroy) {
229
+ const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges)
230
+
231
+ for (const parameterChange of statefulParameterChanges) {
232
+ const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name)!;
233
+ await statefulParameter.remove(parameterChange.previousValue, plan);
234
+ }
235
+ }
236
+
237
+ await this.resource.destroy(plan as DestroyPlan<T>);
238
+ }
239
+
240
+ private validateRefreshResults(refresh: Array<Partial<T>> | null) {
241
+ if (!refresh) {
242
+ return;
243
+ }
244
+
245
+ if (!this.settings.allowMultiple && refresh.length > 1) {
246
+ throw new Error(`Resource: ${this.settings.id}. Allow multiple was set to false but multiple refresh results were returned.
247
+
248
+ ${JSON.stringify(refresh, null, 2)}
249
+ `)
250
+ }
251
+ }
252
+
253
+ private async applyTransformParameters(desired: Partial<T> | null): Promise<void> {
254
+ if (!desired) {
255
+ return;
256
+ }
257
+
258
+ for (const [key, inputTransformation] of Object.entries(this.parsedSettings.inputTransformations)) {
259
+ if (desired[key] === undefined || !inputTransformation) {
260
+ continue;
261
+ }
262
+
263
+ (desired as Record<string, unknown>)[key] = await inputTransformation(desired[key]);
264
+ }
265
+
266
+ if (this.settings.inputTransformation) {
267
+ const transformed = await this.settings.inputTransformation(desired)
268
+ Object.keys(desired).forEach((k) => delete desired[k])
269
+ Object.assign(desired, transformed);
270
+ }
271
+ }
272
+
273
+ private addDefaultValues(desired: Partial<T> | null): void {
274
+ if (!desired) {
275
+ return;
276
+ }
277
+
278
+ for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
279
+ if (defaultValue !== undefined && (desired[key] === undefined || desired[key] === null)) {
280
+ (desired as Record<string, unknown>)[key] = defaultValue;
281
+ }
282
+ }
283
+ }
284
+
285
+ private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Array<Partial<T>> | null> {
286
+ const result = await this.resource.refresh(resourceParameters);
287
+
288
+ const currentParametersArray = Array.isArray(result) || result === null
289
+ ? result
290
+ : [result]
291
+
292
+ this.validateRefreshResults(currentParametersArray);
293
+ return currentParametersArray;
294
+ }
295
+
296
+ // Refresh stateful parameters
297
+ // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
298
+ private async refreshStatefulParameters(statefulParametersConfig: Partial<T>): Promise<Partial<T>> {
299
+ const result: Partial<T> = {}
300
+ const sortedEntries = Object.entries(statefulParametersConfig)
301
+ .sort(
302
+ ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)!
303
+ )
304
+
305
+ for (const [key, desiredValue] of sortedEntries) {
306
+ const statefulParameter = this.parsedSettings.statefulParameters.get(key);
307
+ if (!statefulParameter) {
308
+ throw new Error(`Stateful parameter ${key} was not found`);
309
+ }
310
+
311
+ (result as Record<string, unknown>)[key] = await statefulParameter.refresh(desiredValue ?? null)
312
+ }
313
+
314
+ return result;
315
+ }
316
+
317
+ private validatePlanInputs(
318
+ desired: Partial<T> & ResourceConfig | null,
319
+ current: Partial<T> & ResourceConfig | null,
320
+ statefulMode: boolean,
321
+ ) {
322
+ if (!desired && !current) {
323
+ throw new Error('Desired config and current config cannot both be missing')
324
+ }
325
+
326
+ if (!statefulMode && !desired) {
327
+ throw new Error('Desired config must be provided in non-stateful mode')
328
+ }
329
+ }
330
+
331
+ private getSortedStatefulParameterChanges(parameterChanges: ParameterChange<T>[]) {
332
+ return parameterChanges
333
+ .filter((pc: ParameterChange<T>) => this.parsedSettings.statefulParameters.has(pc.name))
334
+ .sort((a, b) =>
335
+ this.parsedSettings.statefulParameterOrder.get(a.name)! - this.parsedSettings.statefulParameterOrder.get(b.name)!
336
+ )
337
+ }
338
+
339
+ }
340
+