codify-plugin-lib 1.0.71 → 1.0.73

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,11 +1,11 @@
1
1
  import Ajv from 'ajv';
2
2
  import { ValidateFunction } from 'ajv/dist/2020.js';
3
- import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
+ import { ResourceConfig, StringIndexedObject, ValidateResponseData } from 'codify-schemas';
4
4
  import { ParameterChange } from './change-set.js';
5
5
  import { Plan } from './plan.js';
6
6
  import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions } from './plan-types.js';
7
7
  import { ResourceOptions } from './resource-options.js';
8
- import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
8
+ import { ResourceParameterOptions } from './resource-types.js';
9
9
  import { StatefulParameter } from './stateful-parameter.js';
10
10
  import { TransformParameter } from './transform-parameter.js';
11
11
  export declare abstract class Resource<T extends StringIndexedObject> {
@@ -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,7 +35,7 @@ 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>;
38
+ customValidation(parameters: Partial<T>): Promise<void>;
39
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>;
@@ -24,6 +24,7 @@ export class Resource {
24
24
  this.ajv = new Ajv2020.default({
25
25
  strict: true,
26
26
  strictRequired: false,
27
+ allErrors: true,
27
28
  });
28
29
  this.schemaValidator = this.ajv.compile(this.options.schema);
29
30
  }
@@ -37,17 +38,42 @@ export class Resource {
37
38
  this.transformParameterOrder = parser.transformParameterOrder;
38
39
  }
39
40
  async onInitialize() { }
40
- async validateResource(parameters) {
41
+ async validate(parameters, resourceMetaData) {
41
42
  if (this.schemaValidator) {
42
43
  const isValid = this.schemaValidator(parameters);
43
44
  if (!isValid) {
44
45
  return {
45
- errors: this.schemaValidator?.errors ?? [],
46
+ resourceType: resourceMetaData.type,
47
+ resourceName: resourceMetaData.name,
48
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
46
49
  isValid: false,
47
50
  };
48
51
  }
49
52
  }
50
- return this.validate(parameters);
53
+ let isValid = true;
54
+ let customValidationErrorMessage = undefined;
55
+ try {
56
+ await this.customValidation(parameters);
57
+ }
58
+ catch (err) {
59
+ isValid = false;
60
+ customValidationErrorMessage = err.message;
61
+ }
62
+ if (!isValid) {
63
+ return {
64
+ resourceType: resourceMetaData.type,
65
+ resourceName: resourceMetaData.name,
66
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
67
+ customValidationErrorMessage,
68
+ isValid: false,
69
+ };
70
+ }
71
+ return {
72
+ resourceType: resourceMetaData.type,
73
+ resourceName: resourceMetaData.name,
74
+ schemaValidationErrors: [],
75
+ isValid: true,
76
+ };
51
77
  }
52
78
  async plan(desiredConfig, currentConfig = null, statefulMode = false) {
53
79
  this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
@@ -221,11 +247,7 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
221
247
  throw new Error('Desired config must be provided in non-stateful mode');
222
248
  }
223
249
  }
224
- async validate(parameters) {
225
- return {
226
- isValid: true,
227
- };
228
- }
250
+ async customValidation(parameters) { }
229
251
  ;
230
252
  async applyModify(pc, plan) { }
231
253
  ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.71",
3
+ "version": "1.0.73",
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.39",
17
+ "codify-schemas": "1.0.42",
18
18
  "@npmcli/promise-spawn": "^7.0.1"
19
19
  },
20
20
  "devDependencies": {
@@ -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,13 +1,19 @@
1
1
  import Ajv from 'ajv';
2
2
  import Ajv2020, { ValidateFunction } from 'ajv/dist/2020.js';
3
- import { ParameterOperation, ResourceConfig, ResourceOperation, StringIndexedObject, } from 'codify-schemas';
3
+ import {
4
+ ParameterOperation,
5
+ ResourceConfig,
6
+ ResourceOperation,
7
+ StringIndexedObject,
8
+ ValidateResponseData,
9
+ } from 'codify-schemas';
4
10
 
5
11
  import { setsEqual, splitUserConfig } from '../utils/utils.js';
6
12
  import { ParameterChange } from './change-set.js';
7
13
  import { Plan } from './plan.js';
8
14
  import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions, PlanOptions } from './plan-types.js';
9
15
  import { ResourceOptions, ResourceOptionsParser } from './resource-options.js';
10
- import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
16
+ import { ResourceParameterOptions } from './resource-types.js';
11
17
  import { StatefulParameter } from './stateful-parameter.js';
12
18
  import { TransformParameter } from './transform-parameter.js';
13
19
 
@@ -44,6 +50,7 @@ export abstract class Resource<T extends StringIndexedObject> {
44
50
  this.ajv = new Ajv2020.default({
45
51
  strict: true,
46
52
  strictRequired: false,
53
+ allErrors: true,
47
54
  })
48
55
  this.schemaValidator = this.ajv.compile(this.options.schema);
49
56
  }
@@ -60,19 +67,48 @@ export abstract class Resource<T extends StringIndexedObject> {
60
67
 
61
68
  async onInitialize(): Promise<void> {}
62
69
 
63
- async validateResource(parameters: Partial<T>): Promise<ValidationResult> {
70
+ async validate(
71
+ parameters: Partial<T>,
72
+ resourceMetaData: ResourceConfig
73
+ ): Promise<ValidateResponseData['resourceValidations'][0]> {
64
74
  if (this.schemaValidator) {
65
75
  const isValid = this.schemaValidator(parameters);
66
76
 
67
77
  if (!isValid) {
68
78
  return {
69
- errors: this.schemaValidator?.errors ?? [],
79
+ resourceType: resourceMetaData.type,
80
+ resourceName: resourceMetaData.name,
81
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
70
82
  isValid: false,
71
83
  }
72
84
  }
73
85
  }
74
86
 
75
- return this.validate(parameters);
87
+ let isValid = true;
88
+ let customValidationErrorMessage = undefined;
89
+ try {
90
+ await this.customValidation(parameters);
91
+ } catch (err) {
92
+ isValid = false;
93
+ customValidationErrorMessage = (err as Error).message;
94
+ }
95
+
96
+ if (!isValid) {
97
+ return {
98
+ resourceType: resourceMetaData.type,
99
+ resourceName: resourceMetaData.name,
100
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
101
+ customValidationErrorMessage,
102
+ isValid: false,
103
+ }
104
+ }
105
+
106
+ return {
107
+ resourceType: resourceMetaData.type,
108
+ resourceName: resourceMetaData.name,
109
+ schemaValidationErrors: [],
110
+ isValid: true,
111
+ }
76
112
  }
77
113
 
78
114
  // TODO: Currently stateful mode expects that the currentConfig does not need any additional transformations (default and transform parameters)
@@ -342,11 +378,13 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
342
378
  }
343
379
  }
344
380
 
345
- async validate(parameters: Partial<T>): Promise<ValidationResult> {
346
- return {
347
- isValid: true,
348
- }
349
- };
381
+ /**
382
+ * Add custom validation logic in-addition to the default schema validation.
383
+ * In this method throw an error if the object did not validate. The message of the
384
+ * error will be shown to the user.
385
+ * @param parameters
386
+ */
387
+ async customValidation(parameters: Partial<T>): Promise<void> {};
350
388
 
351
389
  abstract refresh(parameters: Partial<T>): Promise<Partial<T> | null>;
352
390
 
@@ -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;