codify-plugin-lib 1.0.70 → 1.0.72

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.
@@ -31,17 +31,15 @@ export class Plugin {
31
31
  if (!this.resources.has(config.type)) {
32
32
  throw new Error(`Resource type not found: ${config.type}`);
33
33
  }
34
- const { parameters } = splitUserConfig(config);
35
- const validateResult = await this.resources.get(config.type).validateResource(parameters);
36
- validationResults.push({
37
- ...validateResult,
38
- resourceType: config.type,
39
- resourceName: config.name,
40
- });
34
+ const { parameters, resourceMetadata } = splitUserConfig(config);
35
+ const validation = await this.resources
36
+ .get(config.type)
37
+ .validate(parameters, resourceMetadata);
38
+ validationResults.push(validation);
41
39
  }
42
40
  await this.crossValidateResources(data.configs);
43
41
  return {
44
- validationResults
42
+ resourceValidations: validationResults
45
43
  };
46
44
  }
47
45
  async plan(data) {
@@ -9,7 +9,3 @@ export interface ResourceDefinition {
9
9
  type: string;
10
10
  };
11
11
  }
12
- export interface ValidationResult {
13
- isValid: boolean;
14
- errors?: unknown[];
15
- }
@@ -1,13 +1,13 @@
1
- import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
1
+ import Ajv from 'ajv';
2
+ import { ValidateFunction } from 'ajv/dist/2020.js';
3
+ import { ResourceConfig, StringIndexedObject, ValidateResponseData } from 'codify-schemas';
2
4
  import { ParameterChange } from './change-set.js';
3
5
  import { Plan } from './plan.js';
4
- import { StatefulParameter } from './stateful-parameter.js';
5
- import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
6
6
  import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions } from './plan-types.js';
7
- import { TransformParameter } from './transform-parameter.js';
8
7
  import { ResourceOptions } from './resource-options.js';
9
- import Ajv from 'ajv';
10
- import { ValidateFunction } from 'ajv/dist/2020.js';
8
+ import { ResourceParameterOptions } from './resource-types.js';
9
+ import { StatefulParameter } from './stateful-parameter.js';
10
+ import { TransformParameter } from './transform-parameter.js';
11
11
  export declare abstract class Resource<T extends StringIndexedObject> {
12
12
  readonly typeId: string;
13
13
  readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
@@ -23,7 +23,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
23
23
  protected schemaValidator?: ValidateFunction;
24
24
  protected constructor(options: ResourceOptions<T>);
25
25
  onInitialize(): Promise<void>;
26
- validateResource(parameters: Partial<T>): Promise<ValidationResult>;
26
+ validate(parameters: Partial<T>, resourceMetaData: ResourceConfig): Promise<ValidateResponseData['resourceValidations'][0]>;
27
27
  plan(desiredConfig: Partial<T> & ResourceConfig | null, currentConfig?: Partial<T> & ResourceConfig | null, statefulMode?: boolean): Promise<Plan<T>>;
28
28
  apply(plan: Plan<T>): Promise<void>;
29
29
  private _applyCreate;
@@ -35,8 +35,8 @@ export declare abstract class Resource<T extends StringIndexedObject> {
35
35
  private refreshNonStatefulParameters;
36
36
  private refreshStatefulParameters;
37
37
  private validatePlanInputs;
38
- validate(parameters: Partial<T>): Promise<ValidationResult>;
39
- abstract refresh(values: Partial<T>): Promise<Partial<T> | null>;
38
+ customValidation(parameters: Partial<T>): Promise<void>;
39
+ abstract refresh(parameters: Partial<T>): Promise<Partial<T> | null>;
40
40
  abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
41
41
  applyModify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void>;
42
42
  abstract applyDestroy(plan: DestroyPlan<T>): Promise<void>;
@@ -1,8 +1,8 @@
1
+ import Ajv2020 from 'ajv/dist/2020.js';
1
2
  import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
2
- import { Plan } from './plan.js';
3
3
  import { setsEqual, splitUserConfig } from '../utils/utils.js';
4
+ import { Plan } from './plan.js';
4
5
  import { ResourceOptionsParser } from './resource-options.js';
5
- import Ajv2020 from 'ajv/dist/2020.js';
6
6
  export class Resource {
7
7
  typeId;
8
8
  statefulParameters;
@@ -37,28 +37,53 @@ export class Resource {
37
37
  this.transformParameterOrder = parser.transformParameterOrder;
38
38
  }
39
39
  async onInitialize() { }
40
- async validateResource(parameters) {
40
+ async validate(parameters, resourceMetaData) {
41
41
  if (this.schemaValidator) {
42
42
  const isValid = this.schemaValidator(parameters);
43
43
  if (!isValid) {
44
44
  return {
45
+ resourceType: resourceMetaData.type,
46
+ resourceName: resourceMetaData.name,
47
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
45
48
  isValid: false,
46
- errors: this.schemaValidator?.errors ?? [],
47
49
  };
48
50
  }
49
51
  }
50
- return this.validate(parameters);
52
+ let isValid = true;
53
+ let customValidationErrorMessage = undefined;
54
+ try {
55
+ await this.customValidation(parameters);
56
+ }
57
+ catch (err) {
58
+ isValid = false;
59
+ customValidationErrorMessage = err.message;
60
+ }
61
+ if (!isValid) {
62
+ return {
63
+ resourceType: resourceMetaData.type,
64
+ resourceName: resourceMetaData.name,
65
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
66
+ customValidationErrorMessage,
67
+ isValid: false,
68
+ };
69
+ }
70
+ return {
71
+ resourceType: resourceMetaData.type,
72
+ resourceName: resourceMetaData.name,
73
+ schemaValidationErrors: [],
74
+ isValid: true,
75
+ };
51
76
  }
52
77
  async plan(desiredConfig, currentConfig = null, statefulMode = false) {
53
78
  this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
54
79
  const planOptions = {
55
- statefulMode,
56
80
  parameterOptions: this.parameterOptions,
81
+ statefulMode,
57
82
  };
58
83
  this.addDefaultValues(desiredConfig);
59
84
  await this.applyTransformParameters(desiredConfig);
60
85
  const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters);
61
- const { desiredParameters, resourceMetadata, nonStatefulParameters, statefulParameters, } = parsedConfig;
86
+ const { desiredParameters, nonStatefulParameters, resourceMetadata, statefulParameters, } = parsedConfig;
62
87
  const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
63
88
  if (currentParameters == null) {
64
89
  return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
@@ -167,21 +192,20 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
167
192
  throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
168
193
  }
169
194
  delete desired[key];
170
- Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
195
+ for (const [tvKey, tvValue] of Object.entries(transformedValue)) {
171
196
  desired[tvKey] = tvValue;
172
- });
197
+ }
173
198
  }
174
199
  }
175
200
  addDefaultValues(desired) {
176
201
  if (!desired) {
177
202
  return;
178
203
  }
179
- Object.entries(this.defaultValues)
180
- .forEach(([key, defaultValue]) => {
204
+ for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
181
205
  if (defaultValue !== undefined && desired[key] === undefined) {
182
206
  desired[key] = defaultValue;
183
207
  }
184
- });
208
+ }
185
209
  }
186
210
  async refreshNonStatefulParameters(resourceParameters) {
187
211
  const currentParameters = await this.refresh(resourceParameters);
@@ -222,11 +246,7 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
222
246
  throw new Error('Desired config must be provided in non-stateful mode');
223
247
  }
224
248
  }
225
- async validate(parameters) {
226
- return {
227
- isValid: true,
228
- };
229
- }
249
+ async customValidation(parameters) { }
230
250
  ;
231
251
  async applyModify(pc, plan) { }
232
252
  ;
@@ -269,18 +289,14 @@ ${JSON.stringify(currentMetadata, null, 2)}`);
269
289
  get parameters() {
270
290
  const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
271
291
  const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
272
- return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) };
292
+ return { ...desiredParameters, ...currentParameters };
273
293
  }
274
294
  get nonStatefulParameters() {
275
- const parameters = this.parameters;
276
- return Object.fromEntries([
277
- ...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
278
- ]);
295
+ const { parameters } = this;
296
+ return Object.fromEntries(Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))));
279
297
  }
280
298
  get statefulParameters() {
281
- const parameters = this.parameters;
282
- return Object.fromEntries([
283
- ...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
284
- ]);
299
+ const { parameters } = this;
300
+ return Object.fromEntries(Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)));
285
301
  }
286
302
  }
@@ -39,8 +39,8 @@ export function isDebug() {
39
39
  export function splitUserConfig(config) {
40
40
  const resourceMetadata = {
41
41
  type: config.type,
42
- ...(config.name && { name: config.name }),
43
- ...(config.dependsOn && { dependsOn: config.dependsOn }),
42
+ ...(config.name ? { name: config.name } : {}),
43
+ ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
44
44
  };
45
45
  const { type, name, dependsOn, ...parameters } = config;
46
46
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.70",
3
+ "version": "1.0.72",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "ajv": "^8.12.0",
16
16
  "ajv-formats": "^2.1.1",
17
- "codify-schemas": "1.0.38",
17
+ "codify-schemas": "1.0.42",
18
18
  "@npmcli/promise-spawn": "^7.0.1"
19
19
  },
20
20
  "devDependencies": {
@@ -121,6 +121,28 @@ describe('Plan entity tests', () => {
121
121
  .every((pc) => pc.operation === ParameterOperation.ADD)
122
122
  ).to.be.true;
123
123
  })
124
+
125
+ it('Returns the original resource names', () => {
126
+ const resource = createResource();
127
+
128
+ const plan = Plan.create(
129
+ {
130
+ propA: 'propA',
131
+ },
132
+ {
133
+ propA: 'propA2',
134
+ },
135
+ {
136
+ type: 'type',
137
+ name: 'name1'
138
+ }, { statefulMode: false });
139
+
140
+ expect(plan.toResponse()).toMatchObject({
141
+ resourceType: 'type',
142
+ resourceName: 'name1',
143
+ operation: ResourceOperation.RECREATE
144
+ })
145
+ })
124
146
  })
125
147
 
126
148
  function createResource(): Resource<any> {
@@ -3,8 +3,7 @@ import { Plugin } from './plugin.js';
3
3
  import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
4
4
  import { Resource } from './resource.js';
5
5
  import { Plan } from './plan.js';
6
- import { ValidationResult } from './resource-types.js';
7
- import { ApplyValidationError } from './errors.js';
6
+ import { spy } from 'sinon';
8
7
 
9
8
  interface TestConfig extends StringIndexedObject {
10
9
  propA: string;
@@ -27,36 +26,19 @@ class TestResource extends Resource<TestConfig> {
27
26
  return Promise.resolve(undefined);
28
27
  }
29
28
 
30
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
29
+ async refresh(): Promise<Partial<TestConfig> | null> {
31
30
  return {
32
31
  propA: 'a',
33
32
  propB: 10,
34
33
  propC: 'c',
35
34
  };
36
35
  }
37
-
38
- async validateResource(config: unknown): Promise<ValidationResult> {
39
- return {
40
- isValid: true
41
- }
42
- }
43
36
  }
44
37
 
45
38
  describe('Plugin tests', () => {
46
- it('Validates that applies were successfully applied', async () => {
47
- const resource= new class extends TestResource {
48
- async applyCreate(plan: Plan<TestConfig>): Promise<void> {
49
- }
50
-
51
- // Refresh has to line up with desired for the apply to go through
52
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
53
- return {
54
- propA: 'abc'
55
- }
56
- }
57
- }
58
-
59
- const plugin = Plugin.create('testPlugin', [resource])
39
+ it('Can apply resource', async () => {
40
+ const resource= spy(new TestResource())
41
+ const plugin = Plugin.create('testPlugin', [resource as any])
60
42
 
61
43
  const plan = {
62
44
  operation: ResourceOperation.CREATE,
@@ -66,45 +48,13 @@ describe('Plugin tests', () => {
66
48
  ]
67
49
  };
68
50
 
69
- // If this doesn't throw then it passes the test
70
51
  await plugin.apply({ plan });
52
+ expect(resource.applyCreate.calledOnce).to.be.true;
71
53
  });
72
54
 
73
- it('Validates that applies were successfully applied (error)', async () => {
74
- const resource = new class extends TestResource {
75
- async applyCreate(plan: Plan<TestConfig>): Promise<void> {
76
- }
77
-
78
- // Return null to indicate that the resource was not created
79
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
80
- return null;
81
- }
82
- }
83
- const plugin = Plugin.create('testPlugin', [resource])
84
-
85
- const plan = {
86
- operation: ResourceOperation.CREATE,
87
- resourceType: 'testResource',
88
- parameters: [
89
- { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null },
90
- ]
91
- };
92
-
93
- await expect(async () => plugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
94
- });
95
-
96
- it('Validates that deletes were successfully applied', async () => {
97
- const resource = new class extends TestResource {
98
- async applyCreate(plan: Plan<TestConfig>): Promise<void> {
99
- }
100
-
101
- // Return null to indicate that the resource was deleted
102
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
103
- return null;
104
- }
105
- }
106
-
107
- const testPlugin = Plugin.create('testPlugin', [resource])
55
+ it('Can destroy resource', async () => {
56
+ const resource = spy(new TestResource());
57
+ const testPlugin = Plugin.create('testPlugin', [resource as any])
108
58
 
109
59
  const plan = {
110
60
  operation: ResourceOperation.DESTROY,
@@ -114,46 +64,13 @@ describe('Plugin tests', () => {
114
64
  ]
115
65
  };
116
66
 
117
- // If this doesn't throw then it passes the test
118
67
  await testPlugin.apply({ plan })
68
+ expect(resource.applyDestroy.calledOnce).to.be.true;
119
69
  });
120
70
 
121
- it('Validates that deletes were successfully applied (error)', async () => {
122
- const resource = new class extends TestResource {
123
- async applyCreate(plan: Plan<TestConfig>): Promise<void> {
124
- }
125
-
126
- // Return a value to indicate that the resource still exists
127
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
128
- return { propA: 'abc' };
129
- }
130
- }
131
-
132
- const testPlugin = Plugin.create('testPlugin', [resource])
133
-
134
- const plan = {
135
- operation: ResourceOperation.DESTROY,
136
- resourceType: 'testResource',
137
- parameters: [
138
- { name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
139
- ]
140
- };
141
-
142
- // If this doesn't throw then it passes the test
143
- expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
144
- });
145
-
146
- it('Validates that re-create was successfully applied', async () => {
147
- const resource = new class extends TestResource {
148
- async applyCreate(plan: Plan<TestConfig>): Promise<void> {
149
- }
150
-
151
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
152
- return { propA: 'def'};
153
- }
154
- }
155
-
156
- const testPlugin = Plugin.create('testPlugin', [resource])
71
+ it('Can re-create resource', async () => {
72
+ const resource = spy(new TestResource())
73
+ const testPlugin = Plugin.create('testPlugin', [resource as any])
157
74
 
158
75
  const plan = {
159
76
  operation: ResourceOperation.RECREATE,
@@ -163,31 +80,24 @@ describe('Plugin tests', () => {
163
80
  ]
164
81
  };
165
82
 
166
- // If this doesn't throw then it passes the test
167
83
  await testPlugin.apply({ plan })
84
+ expect(resource.applyDestroy.calledOnce).to.be.true;
85
+ expect(resource.applyCreate.calledOnce).to.be.true;
168
86
  });
169
87
 
170
- it('Validates that modify was successfully applied (error)', async () => {
171
- const resource = new class extends TestResource {
172
- async applyCreate(plan: Plan<TestConfig>): Promise<void> {
173
- }
174
-
175
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
176
- return { propA: 'abc' };
177
- }
178
- }
179
-
180
- const testPlugin = Plugin.create('testPlugin', [resource])
88
+ it('Can modify resource', async () => {
89
+ const resource = spy(new TestResource())
90
+ const testPlugin = Plugin.create('testPlugin', [resource as any])
181
91
 
182
92
  const plan = {
183
- operation: ResourceOperation.DESTROY,
93
+ operation: ResourceOperation.MODIFY,
184
94
  resourceType: 'testResource',
185
95
  parameters: [
186
- { name: 'propA', operation: ParameterOperation.REMOVE, newValue: 'def', previousValue: 'abc' },
96
+ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
187
97
  ]
188
98
  };
189
99
 
190
- // If this doesn't throw then it passes the test
191
- expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
100
+ await testPlugin.apply({ plan })
101
+ expect(resource.applyModify.calledOnce).to.be.true;
192
102
  });
193
103
  });
@@ -50,19 +50,17 @@ export class Plugin {
50
50
  throw new Error(`Resource type not found: ${config.type}`);
51
51
  }
52
52
 
53
- const { parameters } = splitUserConfig(config);
54
- const validateResult = await this.resources.get(config.type)!.validateResource(parameters);
55
-
56
- validationResults.push({
57
- ...validateResult,
58
- resourceType: config.type,
59
- resourceName: config.name,
60
- });
53
+ const { parameters, resourceMetadata } = splitUserConfig(config);
54
+ const validation = await this.resources
55
+ .get(config.type)!
56
+ .validate(parameters, resourceMetadata);
57
+
58
+ validationResults.push(validation);
61
59
  }
62
60
 
63
61
  await this.crossValidateResources(data.configs);
64
62
  return {
65
- validationResults
63
+ resourceValidations: validationResults
66
64
  };
67
65
  }
68
66
 
@@ -471,9 +471,9 @@ describe('Resource parameter tests', () => {
471
471
  const plan = await resource.plan({ type: 'resourceType', propC: 'abc' } as any);
472
472
 
473
473
  expect(resource.refresh.called).to.be.true;
474
- expect(resource.refresh.getCall(0).firstArg.has('propA')).to.be.true;
475
- expect(resource.refresh.getCall(0).firstArg.has('propB')).to.be.true;
476
- expect(resource.refresh.getCall(0).firstArg.has('propC')).to.be.false;
474
+ expect(resource.refresh.getCall(0).firstArg['propA']).to.exist;
475
+ expect(resource.refresh.getCall(0).firstArg['propB']).to.exist;
476
+ expect(resource.refresh.getCall(0).firstArg['propC']).to.not.exist;
477
477
 
478
478
  expect(plan.desiredConfig?.propA).to.eq('propA');
479
479
  expect(plan.desiredConfig?.propB).to.eq(10);
@@ -513,8 +513,8 @@ describe('Resource parameter tests', () => {
513
513
  const plan = await resource.plan({ type: 'resourceType', propA: 'propA', propB: 10 } as any);
514
514
 
515
515
  expect(transformParameter.transform.called).to.be.false;
516
- expect(resource.refresh.getCall(0).firstArg.has('propA')).to.be.true;
517
- expect(resource.refresh.getCall(0).firstArg.has('propB')).to.be.true;
516
+ expect(resource.refresh.getCall(0).firstArg['propA']).to.exist;
517
+ expect(resource.refresh.getCall(0).firstArg['propB']).to.exist;
518
518
 
519
519
  expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
520
520
  })
@@ -12,7 +12,7 @@ describe('Resource tests for stateful plans', () => {
12
12
  super({ type: 'resource' });
13
13
  }
14
14
 
15
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
15
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
16
16
  return {
17
17
  propA: 'propADifferent',
18
18
  propB: undefined,
@@ -61,7 +61,7 @@ describe('Resource tests for stateful plans', () => {
61
61
  super({ type: 'resource' });
62
62
  }
63
63
 
64
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
64
+ async refresh(): Promise<Partial<TestConfig> | null> {
65
65
  return null;
66
66
  }
67
67
  }
@@ -113,7 +113,7 @@ describe('Resource tests for stateful plans', () => {
113
113
  super({ type: 'resource' });
114
114
  }
115
115
 
116
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
116
+ async refresh(): Promise<Partial<TestConfig> | null> {
117
117
  return {
118
118
  propA: 'propA',
119
119
  propC: 'propC',
@@ -184,7 +184,7 @@ describe('Resource tests for stateful plans', () => {
184
184
  });
185
185
  }
186
186
 
187
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
187
+ async refresh(): Promise<Partial<TestConfig> | null> {
188
188
  return {
189
189
  propA: 'propA',
190
190
  propC: 'propC',
@@ -29,8 +29,3 @@ export interface ResourceDefinition {
29
29
  type: string;
30
30
  }
31
31
  }
32
-
33
- export interface ValidationResult {
34
- isValid: boolean;
35
- errors?: unknown[],
36
- }
@@ -3,7 +3,6 @@ import { ResourceOperation, StringIndexedObject } from 'codify-schemas';
3
3
  import { spy } from 'sinon';
4
4
  import { Plan } from './plan.js';
5
5
  import { describe, expect, it } from 'vitest'
6
- import { ValidationResult } from './resource-types.js';
7
6
  import { StatefulParameter } from './stateful-parameter.js';
8
7
  import { ResourceOptions } from './resource-options.js';
9
8
  import { CreatePlan, DestroyPlan, ModifyPlan } from './plan-types.js';
@@ -28,19 +27,13 @@ export class TestResource extends Resource<TestConfig> {
28
27
  return Promise.resolve(undefined);
29
28
  }
30
29
 
31
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
30
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
32
31
  return {
33
32
  propA: 'a',
34
33
  propB: 10,
35
34
  propC: 'c',
36
35
  };
37
36
  }
38
-
39
- async validateResource(config: unknown): Promise<ValidationResult> {
40
- return {
41
- isValid: true
42
- }
43
- }
44
37
  }
45
38
 
46
39
  describe('Resource tests', () => {
@@ -298,9 +291,8 @@ describe('Resource tests', () => {
298
291
  }
299
292
 
300
293
  // @ts-ignore
301
- async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
302
- expect(desired.has('propA')).to.be.true;
303
- expect(desired.get('propA')).to.be.eq('propADefault');
294
+ async refresh(desired: Partial<TestConfig>): Promise<Partial<TestConfig>> {
295
+ expect(desired['propA']).to.be.eq('propADefault');
304
296
 
305
297
  return {
306
298
  propA: 'propAAfter'
@@ -325,11 +317,11 @@ describe('Resource tests', () => {
325
317
  });
326
318
  }
327
319
 
328
- async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
329
- expect(keys.has('propE')).to.be.true;
320
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
321
+ expect(parameters['propE']).to.exist;
330
322
 
331
323
  return {
332
- propE: keys.get('propE'),
324
+ propE: parameters['propE'],
333
325
  };
334
326
  }
335
327
  }
@@ -374,9 +366,8 @@ describe('Resource tests', () => {
374
366
  }
375
367
 
376
368
  // @ts-ignore
377
- async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
378
- expect(desired.has('propA')).to.be.true;
379
- expect(desired.get('propA')).to.be.eq('propA');
369
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig>> {
370
+ expect(parameters['propA']).to.be.eq('propA');
380
371
 
381
372
  return {
382
373
  propA: 'propAAfter'
@@ -413,7 +404,7 @@ describe('Resource tests', () => {
413
404
  super({ type: 'type' });
414
405
  }
415
406
 
416
- async refresh(values: Map<keyof TestConfig, unknown>): Promise<Partial<TestConfig> | null> {
407
+ async refresh(): Promise<Partial<TestConfig> | null> {
417
408
  return null;
418
409
  }
419
410
 
@@ -1,14 +1,21 @@
1
- import { ParameterOperation, ResourceConfig, ResourceOperation, StringIndexedObject, } from 'codify-schemas';
1
+ import Ajv from 'ajv';
2
+ import Ajv2020, { ValidateFunction } from 'ajv/dist/2020.js';
3
+ import {
4
+ ParameterOperation,
5
+ ResourceConfig,
6
+ ResourceOperation,
7
+ StringIndexedObject,
8
+ ValidateResponseData,
9
+ } from 'codify-schemas';
10
+
11
+ import { setsEqual, splitUserConfig } from '../utils/utils.js';
2
12
  import { ParameterChange } from './change-set.js';
3
13
  import { Plan } from './plan.js';
4
- import { StatefulParameter } from './stateful-parameter.js';
5
- import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
6
- import { setsEqual, splitUserConfig } from '../utils/utils.js';
7
14
  import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions, PlanOptions } from './plan-types.js';
8
- import { TransformParameter } from './transform-parameter.js';
9
15
  import { ResourceOptions, ResourceOptionsParser } from './resource-options.js';
10
- import Ajv from 'ajv';
11
- import Ajv2020, { ValidateFunction } from 'ajv/dist/2020.js';
16
+ import { ResourceParameterOptions } from './resource-types.js';
17
+ import { StatefulParameter } from './stateful-parameter.js';
18
+ import { TransformParameter } from './transform-parameter.js';
12
19
 
13
20
  /**
14
21
  * Description of resource here
@@ -59,19 +66,48 @@ export abstract class Resource<T extends StringIndexedObject> {
59
66
 
60
67
  async onInitialize(): Promise<void> {}
61
68
 
62
- async validateResource(parameters: Partial<T>): Promise<ValidationResult> {
69
+ async validate(
70
+ parameters: Partial<T>,
71
+ resourceMetaData: ResourceConfig
72
+ ): Promise<ValidateResponseData['resourceValidations'][0]> {
63
73
  if (this.schemaValidator) {
64
74
  const isValid = this.schemaValidator(parameters);
65
75
 
66
76
  if (!isValid) {
67
77
  return {
78
+ resourceType: resourceMetaData.type,
79
+ resourceName: resourceMetaData.name,
80
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
68
81
  isValid: false,
69
- errors: this.schemaValidator?.errors ?? [],
70
82
  }
71
83
  }
72
84
  }
73
85
 
74
- return this.validate(parameters);
86
+ let isValid = true;
87
+ let customValidationErrorMessage = undefined;
88
+ try {
89
+ await this.customValidation(parameters);
90
+ } catch (err) {
91
+ isValid = false;
92
+ customValidationErrorMessage = (err as Error).message;
93
+ }
94
+
95
+ if (!isValid) {
96
+ return {
97
+ resourceType: resourceMetaData.type,
98
+ resourceName: resourceMetaData.name,
99
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
100
+ customValidationErrorMessage,
101
+ isValid: false,
102
+ }
103
+ }
104
+
105
+ return {
106
+ resourceType: resourceMetaData.type,
107
+ resourceName: resourceMetaData.name,
108
+ schemaValidationErrors: [],
109
+ isValid: true,
110
+ }
75
111
  }
76
112
 
77
113
  // TODO: Currently stateful mode expects that the currentConfig does not need any additional transformations (default and transform parameters)
@@ -84,8 +120,8 @@ export abstract class Resource<T extends StringIndexedObject> {
84
120
  this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
85
121
 
86
122
  const planOptions: PlanOptions<T> = {
87
- statefulMode,
88
123
  parameterOptions: this.parameterOptions,
124
+ statefulMode,
89
125
  }
90
126
 
91
127
  this.addDefaultValues(desiredConfig);
@@ -95,8 +131,8 @@ export abstract class Resource<T extends StringIndexedObject> {
95
131
  const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters)
96
132
  const {
97
133
  desiredParameters,
98
- resourceMetadata,
99
134
  nonStatefulParameters,
135
+ resourceMetadata,
100
136
  statefulParameters,
101
137
  } = parsedConfig;
102
138
 
@@ -133,13 +169,16 @@ export abstract class Resource<T extends StringIndexedObject> {
133
169
  case ResourceOperation.CREATE: {
134
170
  return this._applyCreate(plan); // TODO: Add new parameters value so that apply
135
171
  }
172
+
136
173
  case ResourceOperation.MODIFY: {
137
174
  return this._applyModify(plan);
138
175
  }
176
+
139
177
  case ResourceOperation.RECREATE: {
140
178
  await this._applyDestroy(plan);
141
179
  return this._applyCreate(plan);
142
180
  }
181
+
143
182
  case ResourceOperation.DESTROY: {
144
183
  return this._applyDestroy(plan);
145
184
  }
@@ -185,11 +224,13 @@ export abstract class Resource<T extends StringIndexedObject> {
185
224
  await statefulParameter.applyAdd(parameterChange.newValue, plan);
186
225
  break;
187
226
  }
227
+
188
228
  case ParameterOperation.MODIFY: {
189
229
  // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
190
230
  await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
191
231
  break;
192
232
  }
233
+
193
234
  case ParameterOperation.REMOVE: {
194
235
  await statefulParameter.applyRemove(parameterChange.previousValue, plan);
195
236
  break;
@@ -256,10 +297,10 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
256
297
  delete desired[key];
257
298
 
258
299
  // Add the new transformed values
259
- Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
300
+ for (const [tvKey, tvValue] of Object.entries(transformedValue)) {
260
301
  // @ts-ignore
261
302
  desired[tvKey] = tvValue;
262
- })
303
+ }
263
304
  }
264
305
  }
265
306
 
@@ -268,13 +309,12 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
268
309
  return;
269
310
  }
270
311
 
271
- Object.entries(this.defaultValues)
272
- .forEach(([key, defaultValue]) => {
312
+ for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
273
313
  if (defaultValue !== undefined && desired[key as any] === undefined) {
274
314
  // @ts-ignore
275
315
  desired[key] = defaultValue;
276
316
  }
277
- });
317
+ }
278
318
  }
279
319
 
280
320
  private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
@@ -337,13 +377,15 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
337
377
  }
338
378
  }
339
379
 
340
- async validate(parameters: Partial<T>): Promise<ValidationResult> {
341
- return {
342
- isValid: true,
343
- }
344
- };
380
+ /**
381
+ * Add custom validation logic in-addition to the default schema validation.
382
+ * In this method throw an error if the object did not validate. The message of the
383
+ * error will be shown to the user.
384
+ * @param parameters
385
+ */
386
+ async customValidation(parameters: Partial<T>): Promise<void> {};
345
387
 
346
- abstract refresh(values: Partial<T>): Promise<Partial<T> | null>;
388
+ abstract refresh(parameters: Partial<T>): Promise<Partial<T> | null>;
347
389
 
348
390
  abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
349
391
 
@@ -407,22 +449,22 @@ ${JSON.stringify(currentMetadata, null, 2)}`);
407
449
  const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
408
450
  const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
409
451
 
410
- return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) } as Partial<T>;
452
+ return { ...desiredParameters, ...currentParameters } as Partial<T>;
411
453
  }
412
454
 
413
455
  get nonStatefulParameters(): Partial<T> {
414
- const parameters = this.parameters;
456
+ const { parameters } = this;
415
457
 
416
- return Object.fromEntries([
417
- ...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
418
- ]) as Partial<T>;
458
+ return Object.fromEntries(
459
+ Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key)))
460
+ ) as Partial<T>;
419
461
  }
420
462
 
421
463
  get statefulParameters(): Partial<T> {
422
- const parameters = this.parameters;
464
+ const { parameters } = this;
423
465
 
424
- return Object.fromEntries([
425
- ...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
426
- ]) as Partial<T>;
466
+ return Object.fromEntries(
467
+ Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key))
468
+ ) as Partial<T>;
427
469
  }
428
470
  }
@@ -22,7 +22,6 @@ class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
22
22
  }
23
23
  }
24
24
 
25
-
26
25
  describe('Stateful parameter tests', () => {
27
26
  it('applyAddItem is called the correct number of times', async () => {
28
27
  const plan = Plan.create<TestConfig>(
@@ -36,7 +35,7 @@ describe('Stateful parameter tests', () => {
36
35
  expect(plan.changeSet.parameterChanges.length).to.eq(1);
37
36
 
38
37
  const testParameter = spy(new TestArrayParameter());
39
- await testParameter.applyAdd(plan.desiredConfig.propA, plan);
38
+ await testParameter.applyAdd(plan.desiredConfig!.propA, plan);
40
39
 
41
40
  expect(testParameter.applyAddItem.callCount).to.eq(3);
42
41
  expect(testParameter.applyRemoveItem.called).to.be.false;
@@ -54,7 +53,7 @@ describe('Stateful parameter tests', () => {
54
53
  expect(plan.changeSet.parameterChanges.length).to.eq(1);
55
54
 
56
55
  const testParameter = spy(new TestArrayParameter());
57
- await testParameter.applyRemove(plan.currentConfig.propA, plan);
56
+ await testParameter.applyRemove(plan.currentConfig!.propA, plan);
58
57
 
59
58
  expect(testParameter.applyAddItem.called).to.be.false;
60
59
  expect(testParameter.applyRemoveItem.callCount).to.eq(3);
@@ -77,7 +76,7 @@ describe('Stateful parameter tests', () => {
77
76
  })
78
77
 
79
78
  const testParameter = spy(new TestArrayParameter());
80
- await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
79
+ await testParameter.applyModify(plan.desiredConfig!.propA, plan.currentConfig!.propA, false, plan);
81
80
 
82
81
  expect(testParameter.applyAddItem.calledThrice).to.be.true;
83
82
  expect(testParameter.applyRemoveItem.called).to.be.false;
@@ -107,7 +106,7 @@ describe('Stateful parameter tests', () => {
107
106
  }
108
107
  });
109
108
 
110
- await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
109
+ await testParameter.applyModify(plan.desiredConfig!.propA, plan.currentConfig!.propA, false, plan);
111
110
 
112
111
  expect(testParameter.applyAddItem.calledOnce).to.be.true;
113
112
  expect(testParameter.applyRemoveItem.called).to.be.false;
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { splitUserConfig } from './utils.js';
3
+
4
+ describe('Utils tests', () => {
5
+ it('Can split a config correctly', () => {
6
+ const { parameters, resourceMetadata } = splitUserConfig({
7
+ type: 'type',
8
+ name: 'name',
9
+ dependsOn: ['a', 'b', 'c'],
10
+ propA: 'propA',
11
+ propB: 'propB',
12
+ propC: 'propC',
13
+ propD: 'propD',
14
+ })
15
+
16
+ expect(resourceMetadata).toMatchObject({
17
+ type: 'type',
18
+ name: 'name',
19
+ dependsOn: ['a', 'b', 'c'],
20
+ })
21
+
22
+ expect(parameters).toMatchObject({
23
+ propA: 'propA',
24
+ propB: 'propB',
25
+ propC: 'propC',
26
+ propD: 'propD',
27
+ })
28
+ })
29
+ })
@@ -85,8 +85,8 @@ export function splitUserConfig<T extends StringIndexedObject>(
85
85
  ): { parameters: T; resourceMetadata: ResourceConfig} {
86
86
  const resourceMetadata = {
87
87
  type: config.type,
88
- ...(config.name && { name: config.name }),
89
- ...(config.dependsOn && { dependsOn: config.dependsOn }),
88
+ ...(config.name ? { name: config.name } : {}),
89
+ ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
90
90
  };
91
91
 
92
92
  const { type, name, dependsOn, ...parameters } = config;
@@ -1 +0,0 @@
1
- export {};
@@ -1,22 +0,0 @@
1
- import { Resource } from './resource.js';
2
- class Test extends Resource {
3
- validate(config) {
4
- throw new Error('Method not implemented.');
5
- }
6
- async refresh(keys) {
7
- const result = {};
8
- if (keys.has('propA')) {
9
- result['propA'] = 'abc';
10
- }
11
- return result;
12
- }
13
- applyCreate(plan) {
14
- throw new Error('Method not implemented.');
15
- }
16
- applyModify(parameterName, newValue, previousValue, plan) {
17
- throw new Error('Method not implemented.');
18
- }
19
- applyDestroy(plan) {
20
- throw new Error('Method not implemented.');
21
- }
22
- }
@@ -1,3 +0,0 @@
1
- export interface StringIndexedObject {
2
- [x: string]: unknown;
3
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,5 +0,0 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- import { ChildProcess } from 'child_process';
3
- export declare class CodifyTestUtils {
4
- static sendMessageToProcessAwaitResponse(process: ChildProcess, message: any): Promise<any>;
5
- }
@@ -1,17 +0,0 @@
1
- export class CodifyTestUtils {
2
- static sendMessageToProcessAwaitResponse(process, message) {
3
- return new Promise((resolve, reject) => {
4
- process.on('message', (response) => {
5
- resolve(response);
6
- });
7
- process.on('error', (err) => reject(err));
8
- process.on('exit', (code) => {
9
- if (code != 0) {
10
- reject('Exit code is not 0');
11
- }
12
- resolve(code);
13
- });
14
- process.send(message);
15
- });
16
- }
17
- }