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,353 @@
1
+ import {
2
+ ApplyRequestData,
3
+ ParameterOperation,
4
+ PlanResponseData,
5
+ ResourceConfig,
6
+ ResourceOperation,
7
+ StringIndexedObject,
8
+ } from 'codify-schemas';
9
+ import { v4 as uuidV4 } from 'uuid';
10
+
11
+ import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
12
+ import { ArrayParameterSetting, ResourceSettings, StatefulParameterSetting } from '../resource/resource-settings.js';
13
+ import { ChangeSet } from './change-set.js';
14
+
15
+ /**
16
+ * A plan represents a set of actions that after taken will turn the current resource into the desired one.
17
+ * A plan consists of list of parameter level changes (ADD, REMOVE, MODIFY or NO-OP) as well as a resource level
18
+ * operation (CREATE, DESTROY, MODIFY, RE-CREATE, NO-OP).
19
+ */
20
+ export class Plan<T extends StringIndexedObject> {
21
+ id: string;
22
+ changeSet: ChangeSet<T>;
23
+ coreParameters: ResourceConfig
24
+
25
+ constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig) {
26
+ this.id = id;
27
+ this.changeSet = changeSet;
28
+ this.coreParameters = resourceMetadata;
29
+ }
30
+
31
+ /**
32
+ * The desired config that a plan will achieve after executing all the actions.
33
+ */
34
+ get desiredConfig(): T | null {
35
+ if (this.changeSet.operation === ResourceOperation.DESTROY) {
36
+ return null;
37
+ }
38
+
39
+ return {
40
+ ...this.coreParameters,
41
+ ...this.changeSet.desiredParameters,
42
+ }
43
+ }
44
+
45
+ /**
46
+ * The current config that the plan is changing.
47
+ */
48
+ get currentConfig(): T | null {
49
+ if (this.changeSet.operation === ResourceOperation.CREATE) {
50
+ return null;
51
+ }
52
+
53
+ return {
54
+ ...this.coreParameters,
55
+ ...this.changeSet.currentParameters,
56
+ }
57
+ }
58
+
59
+ /**
60
+ * When multiples of the same resource are allowed, this matching function will match a given config with one of the
61
+ * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
62
+ * the application name and location to match it to our desired configs name and location.
63
+ *
64
+ * @param params
65
+ * @private
66
+ */
67
+ private static matchCurrentParameters<T extends StringIndexedObject>(params: {
68
+ desiredParameters: Partial<T> | null,
69
+ currentParametersArray: Partial<T>[] | null,
70
+ stateParameters: Partial<T> | null,
71
+ settings: ResourceSettings<T>,
72
+ statefulMode: boolean,
73
+ }): Partial<T> | null {
74
+ const {
75
+ desiredParameters,
76
+ currentParametersArray,
77
+ stateParameters,
78
+ settings,
79
+ statefulMode
80
+ } = params;
81
+
82
+ if (!settings.allowMultiple) {
83
+ return currentParametersArray?.[0] ?? null;
84
+ }
85
+
86
+ if (!currentParametersArray) {
87
+ return null;
88
+ }
89
+
90
+ if (statefulMode) {
91
+ return stateParameters
92
+ ? settings.allowMultiple.matcher(stateParameters, currentParametersArray)
93
+ : null
94
+ }
95
+
96
+ return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray);
97
+ }
98
+
99
+ /**
100
+ * The type (id) of the resource
101
+ *
102
+ * @return string
103
+ */
104
+ getResourceType(): string {
105
+ return this.coreParameters.type
106
+ }
107
+
108
+ static calculate<T extends StringIndexedObject>(params: {
109
+ desiredParameters: Partial<T> | null,
110
+ currentParametersArray: Partial<T>[] | null,
111
+ stateParameters: Partial<T> | null,
112
+ coreParameters: ResourceConfig,
113
+ settings: ParsedResourceSettings<T>,
114
+ statefulMode: boolean,
115
+ }): Plan<T> {
116
+ const {
117
+ desiredParameters,
118
+ currentParametersArray,
119
+ stateParameters,
120
+ coreParameters,
121
+ settings,
122
+ statefulMode
123
+ } = params
124
+
125
+ const currentParameters = Plan.matchCurrentParameters<T>({
126
+ desiredParameters,
127
+ currentParametersArray,
128
+ stateParameters,
129
+ settings,
130
+ statefulMode
131
+ });
132
+
133
+ const filteredCurrentParameters = Plan.filterCurrentParams<T>({
134
+ desiredParameters,
135
+ currentParameters,
136
+ stateParameters,
137
+ settings,
138
+ statefulMode
139
+ });
140
+
141
+ // Empty
142
+ if (!filteredCurrentParameters && !desiredParameters) {
143
+ return new Plan(
144
+ uuidV4(),
145
+ ChangeSet.empty<T>(),
146
+ coreParameters,
147
+ )
148
+ }
149
+
150
+ // CREATE
151
+ if (!filteredCurrentParameters && desiredParameters) {
152
+ return new Plan(
153
+ uuidV4(),
154
+ ChangeSet.create(desiredParameters),
155
+ coreParameters
156
+ )
157
+ }
158
+
159
+ // DESTROY
160
+ if (filteredCurrentParameters && !desiredParameters) {
161
+ return new Plan(
162
+ uuidV4(),
163
+ ChangeSet.destroy(filteredCurrentParameters),
164
+ coreParameters
165
+ )
166
+ }
167
+
168
+ // NO-OP, MODIFY or RE-CREATE
169
+ const changeSet = ChangeSet.calculateModification(
170
+ desiredParameters!,
171
+ filteredCurrentParameters!,
172
+ settings.parameterSettings,
173
+ );
174
+
175
+ return new Plan(
176
+ uuidV4(),
177
+ changeSet,
178
+ coreParameters,
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Only keep relevant params for the plan. We don't want to change settings that were not already
184
+ * defined.
185
+ *
186
+ * 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
187
+ * 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
188
+ * or wants to set. If a parameter is not specified then it's not managed by Codify.
189
+ */
190
+ private static filterCurrentParams<T extends StringIndexedObject>(params: {
191
+ desiredParameters: Partial<T> | null,
192
+ currentParameters: Partial<T> | null,
193
+ stateParameters: Partial<T> | null,
194
+ settings: ResourceSettings<T>,
195
+ statefulMode: boolean,
196
+ }): Partial<T> | null {
197
+ const {
198
+ desiredParameters: desired,
199
+ currentParameters: current,
200
+ stateParameters: state,
201
+ settings,
202
+ statefulMode
203
+ } = params;
204
+
205
+ if (!current) {
206
+ return null;
207
+ }
208
+
209
+ const filteredCurrent = filterCurrent()
210
+ if (!filteredCurrent) {
211
+ return null
212
+ }
213
+
214
+ // For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
215
+ // requires additional filtering for stateful parameter arrays and objects.
216
+ if (statefulMode) {
217
+ return filteredCurrent;
218
+ }
219
+
220
+ // TODO: Add object handling here in addition to arrays in the future
221
+ const arrayStatefulParameters = Object.fromEntries(
222
+ Object.entries(filteredCurrent)
223
+ .filter(([k, v]) => isArrayStatefulParameter(k, v))
224
+ .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
225
+ )
226
+
227
+ return { ...filteredCurrent, ...arrayStatefulParameters }
228
+
229
+ function filterCurrent(): Partial<T> | null {
230
+ if (!current) {
231
+ return null;
232
+ }
233
+
234
+ if (statefulMode) {
235
+ const keys = new Set([...Object.keys(state ?? {}), ...Object.keys(desired ?? {})]);
236
+ return Object.fromEntries(
237
+ Object.entries(current)
238
+ .filter(([k]) => keys.has(k))
239
+ ) as Partial<T>;
240
+ }
241
+
242
+ // Stateless mode
243
+ const keys = new Set(Object.keys(desired ?? {}));
244
+ return Object.fromEntries(
245
+ Object.entries(current)
246
+ .filter(([k]) => keys.has(k))
247
+ ) as Partial<T>;
248
+ }
249
+
250
+ function isArrayStatefulParameter(k: string, v: T[keyof T]): boolean {
251
+ return settings.parameterSettings?.[k]?.type === 'stateful'
252
+ && (settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings().type === 'array'
253
+ && Array.isArray(v)
254
+ }
255
+
256
+ function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
257
+ const desiredArray = desired![k] as unknown[];
258
+ const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
259
+ .definition
260
+ .getSettings() as ArrayParameterSetting)
261
+ .isElementEqual;
262
+
263
+ return v.filter((cv) =>
264
+ desiredArray.find((dv) => (matcher ?? ((a: any, b: any) => a === b))(dv, cv))
265
+ )
266
+ }
267
+ }
268
+
269
+ // TODO: This needs to be revisited. I don't think this is valid anymore.
270
+ // 1. For all scenarios, there shouldn't be an apply without a plan beforehand
271
+ // 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
272
+ static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T> {
273
+ if (!data) {
274
+ throw new Error('Data is empty');
275
+ }
276
+
277
+ addDefaultValues();
278
+
279
+ return new Plan(
280
+ uuidV4(),
281
+ new ChangeSet<T>(
282
+ data.operation,
283
+ data.parameters
284
+ ),
285
+ {
286
+ type: data.resourceType,
287
+ name: data.resourceName,
288
+ },
289
+ );
290
+
291
+ function addDefaultValues(): void {
292
+ Object.entries(defaultValues ?? {})
293
+ .forEach(([key, defaultValue]) => {
294
+ const configValueExists = data!
295
+ .parameters
296
+ .some((p) => p.name === key);
297
+
298
+ // Only set default values if the value does not exist in the config
299
+ if (configValueExists) {
300
+ return;
301
+ }
302
+
303
+ switch (data!.operation) {
304
+ case ResourceOperation.CREATE: {
305
+ data!.parameters.push({
306
+ name: key,
307
+ operation: ParameterOperation.ADD,
308
+ previousValue: null,
309
+ newValue: defaultValue,
310
+ });
311
+ break;
312
+ }
313
+
314
+ case ResourceOperation.DESTROY: {
315
+ data!.parameters.push({
316
+ name: key,
317
+ operation: ParameterOperation.REMOVE,
318
+ previousValue: defaultValue,
319
+ newValue: null,
320
+ });
321
+ break;
322
+ }
323
+
324
+ case ResourceOperation.MODIFY:
325
+ case ResourceOperation.RECREATE:
326
+ case ResourceOperation.NOOP: {
327
+ data!.parameters.push({
328
+ name: key,
329
+ operation: ParameterOperation.NOOP,
330
+ previousValue: defaultValue,
331
+ newValue: defaultValue,
332
+ });
333
+ break;
334
+ }
335
+ }
336
+ });
337
+ }
338
+
339
+ }
340
+
341
+ /**
342
+ * Convert the plan to a JSON response object
343
+ */
344
+ toResponse(): PlanResponseData {
345
+ return {
346
+ planId: this.id,
347
+ operation: this.changeSet.operation,
348
+ resourceName: this.coreParameters.name,
349
+ resourceType: this.coreParameters.type,
350
+ parameters: this.changeSet.parameterChanges,
351
+ }
352
+ }
353
+ }
@@ -1,9 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Plugin } from './plugin.js';
3
3
  import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
4
- import { Resource } from './resource.js';
5
- import { Plan } from './plan.js';
4
+ import { Resource } from '../resource/resource.js';
5
+ import { Plan } from '../plan/plan.js';
6
6
  import { spy } from 'sinon';
7
+ import { ResourceSettings } from '../resource/resource-settings.js';
7
8
 
8
9
  interface TestConfig extends StringIndexedObject {
9
10
  propA: string;
@@ -12,17 +13,17 @@ interface TestConfig extends StringIndexedObject {
12
13
  }
13
14
 
14
15
  class TestResource extends Resource<TestConfig> {
15
- constructor() {
16
- super({
17
- type: 'testResource'
18
- });
16
+ getSettings(): ResourceSettings<TestConfig> {
17
+ return {
18
+ id: 'testResource'
19
+ };
19
20
  }
20
21
 
21
- applyCreate(plan: Plan<TestConfig>): Promise<void> {
22
+ create(plan: Plan<TestConfig>): Promise<void> {
22
23
  return Promise.resolve(undefined);
23
24
  }
24
25
 
25
- applyDestroy(plan: Plan<TestConfig>): Promise<void> {
26
+ destroy(plan: Plan<TestConfig>): Promise<void> {
26
27
  return Promise.resolve(undefined);
27
28
  }
28
29
 
@@ -49,7 +50,7 @@ describe('Plugin tests', () => {
49
50
  };
50
51
 
51
52
  await plugin.apply({ plan });
52
- expect(resource.applyCreate.calledOnce).to.be.true;
53
+ expect(resource.create.calledOnce).to.be.true;
53
54
  });
54
55
 
55
56
  it('Can destroy resource', async () => {
@@ -65,7 +66,7 @@ describe('Plugin tests', () => {
65
66
  };
66
67
 
67
68
  await testPlugin.apply({ plan })
68
- expect(resource.applyDestroy.calledOnce).to.be.true;
69
+ expect(resource.destroy.calledOnce).to.be.true;
69
70
  });
70
71
 
71
72
  it('Can re-create resource', async () => {
@@ -81,8 +82,8 @@ describe('Plugin tests', () => {
81
82
  };
82
83
 
83
84
  await testPlugin.apply({ plan })
84
- expect(resource.applyDestroy.calledOnce).to.be.true;
85
- expect(resource.applyCreate.calledOnce).to.be.true;
85
+ expect(resource.destroy.calledOnce).to.be.true;
86
+ expect(resource.create.calledOnce).to.be.true;
86
87
  });
87
88
 
88
89
  it('Can modify resource', async () => {
@@ -98,6 +99,6 @@ describe('Plugin tests', () => {
98
99
  };
99
100
 
100
101
  await testPlugin.apply({ plan })
101
- expect(resource.applyModify.calledOnce).to.be.true;
102
+ expect(resource.modify.calledOnce).to.be.true;
102
103
  });
103
104
  });
@@ -1,4 +1,3 @@
1
- import { Resource } from './resource.js';
2
1
  import {
3
2
  ApplyRequestData,
4
3
  InitializeResponseData,
@@ -8,37 +7,43 @@ import {
8
7
  ValidateRequestData,
9
8
  ValidateResponseData
10
9
  } from 'codify-schemas';
11
- import { Plan } from './plan.js';
10
+
11
+ import { Plan } from '../plan/plan.js';
12
+ import { Resource } from '../resource/resource.js';
13
+ import { ResourceController } from '../resource/resource-controller.js';
12
14
  import { splitUserConfig } from '../utils/utils.js';
13
15
 
14
16
  export class Plugin {
15
17
  planStorage: Map<string, Plan<any>>;
16
18
 
17
- static create(name: string, resources: Resource<any>[]) {
18
- const resourceMap = new Map<string, Resource<any>>(
19
- resources.map((r) => [r.typeId, r] as const)
20
- );
21
-
22
- return new Plugin(name, resourceMap);
23
- }
24
-
25
19
  constructor(
26
20
  public name: string,
27
- public resources: Map<string, Resource<ResourceConfig>>
21
+ public resourceControllers: Map<string, ResourceController<ResourceConfig>>
28
22
  ) {
29
23
  this.planStorage = new Map();
30
24
  }
31
25
 
26
+ static create(name: string, resources: Resource<any>[]) {
27
+ const controllers = resources
28
+ .map((resource) => new ResourceController(resource))
29
+
30
+ const controllersMap = new Map<string, ResourceController<any>>(
31
+ controllers.map((r) => [r.typeId, r] as const)
32
+ );
33
+
34
+ return new Plugin(name, controllersMap);
35
+ }
36
+
32
37
  async initialize(): Promise<InitializeResponseData> {
33
- for (const resource of this.resources.values()) {
34
- await resource.onInitialize();
38
+ for (const controller of this.resourceControllers.values()) {
39
+ await controller.initialize();
35
40
  }
36
41
 
37
42
  return {
38
- resourceDefinitions: [...this.resources.values()]
43
+ resourceDefinitions: [...this.resourceControllers.values()]
39
44
  .map((r) => ({
40
- type: r.typeId,
41
45
  dependencies: r.dependencies,
46
+ type: r.typeId,
42
47
  }))
43
48
  }
44
49
  }
@@ -46,14 +51,14 @@ export class Plugin {
46
51
  async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
47
52
  const validationResults = [];
48
53
  for (const config of data.configs) {
49
- if (!this.resources.has(config.type)) {
54
+ if (!this.resourceControllers.has(config.type)) {
50
55
  throw new Error(`Resource type not found: ${config.type}`);
51
56
  }
52
57
 
53
- const { parameters, resourceMetadata } = splitUserConfig(config);
54
- const validation = await this.resources
58
+ const { parameters, coreParameters } = splitUserConfig(config);
59
+ const validation = await this.resourceControllers
55
60
  .get(config.type)!
56
- .validate(parameters, resourceMetadata);
61
+ .validate(parameters, coreParameters);
57
62
 
58
63
  validationResults.push(validation);
59
64
  }
@@ -67,11 +72,11 @@ export class Plugin {
67
72
  async plan(data: PlanRequestData): Promise<PlanResponseData> {
68
73
  const type = data.desired?.type ?? data.state?.type
69
74
 
70
- if (!type || !this.resources.has(type)) {
75
+ if (!type || !this.resourceControllers.has(type)) {
71
76
  throw new Error(`Resource type not found: ${type}`);
72
77
  }
73
78
 
74
- const plan = await this.resources.get(type)!.plan(
79
+ const plan = await this.resourceControllers.get(type)!.plan(
75
80
  data.desired ?? null,
76
81
  data.state ?? null,
77
82
  data.isStateful
@@ -83,12 +88,12 @@ export class Plugin {
83
88
 
84
89
  async apply(data: ApplyRequestData): Promise<void> {
85
90
  if (!data.planId && !data.plan) {
86
- throw new Error(`For applies either plan or planId must be supplied`);
91
+ throw new Error('For applies either plan or planId must be supplied');
87
92
  }
88
93
 
89
94
  const plan = this.resolvePlan(data);
90
95
 
91
- const resource = this.resources.get(plan.getResourceType());
96
+ const resource = this.resourceControllers.get(plan.getResourceType());
92
97
  if (!resource) {
93
98
  throw new Error('Malformed plan with resource that cannot be found');
94
99
  }
@@ -97,7 +102,7 @@ export class Plugin {
97
102
  }
98
103
 
99
104
  private resolvePlan(data: ApplyRequestData): Plan<ResourceConfig> {
100
- const { planId, plan: planRequest } = data;
105
+ const { plan: planRequest, planId } = data;
101
106
 
102
107
  if (planId) {
103
108
  if (!this.planStorage.has(planId)) {
@@ -107,12 +112,12 @@ export class Plugin {
107
112
  return this.planStorage.get(planId)!
108
113
  }
109
114
 
110
- if (!planRequest?.resourceType || !this.resources.has(planRequest.resourceType)) {
115
+ if (!planRequest?.resourceType || !this.resourceControllers.has(planRequest.resourceType)) {
111
116
  throw new Error('Malformed plan. Resource type must be supplied or resource type was not found');
112
117
  }
113
118
 
114
- const resource = this.resources.get(planRequest.resourceType)!;
115
- return Plan.fromResponse(data.plan, resource.defaultValues);
119
+ const resource = this.resourceControllers.get(planRequest.resourceType)!;
120
+ return Plan.fromResponse(planRequest, resource.parsedSettings.defaultValues);
116
121
  }
117
122
 
118
123
  protected async crossValidateResources(configs: ResourceConfig[]): Promise<void> {}
@@ -0,0 +1,77 @@
1
+ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
2
+
3
+ import { splitUserConfig } from '../utils/utils.js';
4
+ import { StatefulParameter } from './stateful-parameter.js';
5
+
6
+ export class ConfigParser<T extends StringIndexedObject> {
7
+ private readonly desiredConfig: Partial<T> & ResourceConfig | null;
8
+ private readonly stateConfig: Partial<T> & ResourceConfig | null;
9
+ private statefulParametersMap: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
10
+
11
+ constructor(
12
+ desiredConfig: Partial<T> & ResourceConfig | null,
13
+ stateConfig: Partial<T> & ResourceConfig | null,
14
+ statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>,
15
+ ) {
16
+ this.desiredConfig = desiredConfig;
17
+ this.stateConfig = stateConfig
18
+ this.statefulParametersMap = statefulParameters;
19
+ }
20
+
21
+ get coreParameters(): ResourceConfig {
22
+ const desiredCoreParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).coreParameters : undefined;
23
+ const currentCoreParameters = this.stateConfig ? splitUserConfig(this.stateConfig).coreParameters : undefined;
24
+
25
+ if (!desiredCoreParameters && !currentCoreParameters) {
26
+ throw new Error(`Unable to parse resource core parameters from:
27
+
28
+ Desired: ${JSON.stringify(this.desiredConfig, null, 2)}
29
+
30
+ Current: ${JSON.stringify(this.stateConfig, null, 2)}`)
31
+ }
32
+
33
+ return desiredCoreParameters ?? currentCoreParameters!;
34
+ }
35
+
36
+ get desiredParameters(): Partial<T> | null {
37
+ if (!this.desiredConfig) {
38
+ return null;
39
+ }
40
+
41
+ const { parameters } = splitUserConfig(this.desiredConfig);
42
+ return parameters;
43
+ }
44
+
45
+ get stateParameters(): Partial<T> | null {
46
+ if (!this.stateConfig) {
47
+ return null;
48
+ }
49
+
50
+ const { parameters } = splitUserConfig(this.stateConfig);
51
+ return parameters;
52
+ }
53
+
54
+
55
+ get allParameters(): Partial<T> {
56
+ return { ...this.desiredParameters, ...this.stateParameters } as Partial<T>;
57
+ }
58
+
59
+ get allNonStatefulParameters(): Partial<T> {
60
+ const {
61
+ allParameters,
62
+ statefulParametersMap,
63
+ } = this;
64
+
65
+ return Object.fromEntries(
66
+ Object.entries(allParameters).filter(([key]) => !statefulParametersMap.has(key))
67
+ ) as Partial<T>;
68
+ }
69
+
70
+ get allStatefulParameters(): Partial<T> {
71
+ const { allParameters, statefulParametersMap } = this;
72
+
73
+ return Object.fromEntries(
74
+ Object.entries(allParameters).filter(([key]) => statefulParametersMap.has(key))
75
+ ) as Partial<T>;
76
+ }
77
+ }
@@ -1,20 +1,21 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { ResourceOptions, ResourceOptionsParser } from './resource-options.js';
3
- import { TestConfig } from './resource.test.js';
2
+ import { ResourceSettings } from './resource-settings.js';
3
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
4
+ import { TestConfig } from '../utils/test-utils.test.js';
4
5
 
5
6
  describe('Resource options parser tests', () => {
6
7
  it('Parses default values from options', () => {
7
- const option: ResourceOptions<TestConfig> = {
8
- type: 'typeId',
9
- parameterOptions: {
8
+ const option: ResourceSettings<TestConfig> = {
9
+ id: 'typeId',
10
+ parameterSettings: {
10
11
  propA: { default: 'propA' },
11
12
  propB: { default: 'propB' },
12
13
  propC: { isEqual: () => true },
13
- propD: { },
14
+ propD: {},
14
15
  }
15
16
  }
16
17
 
17
- const result = new ResourceOptionsParser(option);
18
+ const result = new ParsedResourceSettings(option);
18
19
  expect(result.defaultValues).to.deep.eq({
19
20
  propA: 'propA',
20
21
  propB: 'propB'