codify-plugin-lib 1.0.65 → 1.0.67

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.
@@ -59,8 +59,8 @@ export class ChangeSet {
59
59
  }
60
60
  static calculateStatefulModeChangeSet(desired, current, parameterOptions) {
61
61
  const parameterChangeSet = new Array();
62
- const _desired = { ...desired };
63
- const _current = { ...current };
62
+ const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
63
+ const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
64
64
  this.addDefaultValues(_desired, parameterOptions);
65
65
  for (const [k, v] of Object.entries(_current)) {
66
66
  if (_desired[k] == null) {
@@ -108,8 +108,8 @@ export class ChangeSet {
108
108
  }
109
109
  static calculateStatelessModeChangeSet(desired, current, parameterOptions) {
110
110
  const parameterChangeSet = new Array();
111
- const _desired = { ...desired };
112
- const _current = { ...current };
111
+ const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
112
+ const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
113
113
  this.addDefaultValues(_desired, parameterOptions);
114
114
  for (const [k, v] of Object.entries(_desired)) {
115
115
  if (_current[k] == null) {
@@ -1,3 +1,5 @@
1
+ import { Plan } from './plan.js';
2
+ import { StringIndexedObject } from 'codify-schemas';
1
3
  export interface ParameterOptions {
2
4
  modifyOnChange?: boolean;
3
5
  isEqual?: (desired: any, current: any) => boolean;
@@ -9,3 +11,9 @@ export interface PlanOptions<T> {
9
11
  statefulMode: boolean;
10
12
  parameterOptions?: Record<keyof T, ParameterOptions>;
11
13
  }
14
+ export type WithRequired<T, K extends keyof T> = T & {
15
+ [P in K]-?: T[P];
16
+ };
17
+ export type CreatePlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'desiredConfig'>;
18
+ export type DestroyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig'>;
19
+ export type ModifyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig' | 'desiredConfig'>;
@@ -9,7 +9,7 @@ export declare class Plan<T extends StringIndexedObject> {
9
9
  static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, options: PlanOptions<T>): Plan<T>;
10
10
  getResourceType(): string;
11
11
  static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
12
- get desiredConfig(): T;
13
- get currentConfig(): T;
12
+ get desiredConfig(): T | null;
13
+ get currentConfig(): T | null;
14
14
  toResponse(): PlanResponseData;
15
15
  }
@@ -98,12 +98,18 @@ export class Plan {
98
98
  }
99
99
  }
100
100
  get desiredConfig() {
101
+ if (this.changeSet.operation === ResourceOperation.DESTROY) {
102
+ return null;
103
+ }
101
104
  return {
102
105
  ...this.resourceMetadata,
103
106
  ...this.changeSet.desiredParameters,
104
107
  };
105
108
  }
106
109
  get currentConfig() {
110
+ if (this.changeSet.operation === ResourceOperation.CREATE) {
111
+ return null;
112
+ }
107
113
  return {
108
114
  ...this.resourceMetadata,
109
115
  ...this.changeSet.currentParameters,
@@ -64,7 +64,7 @@ export class Plugin {
64
64
  throw new Error('Malformed plan with resource that cannot be found');
65
65
  }
66
66
  await resource.apply(plan);
67
- const validationPlan = await resource.plan({ ...plan.resourceMetadata, ...plan.desiredConfig });
67
+ const validationPlan = await resource.plan(plan.desiredConfig, plan.currentConfig, true);
68
68
  if (validationPlan.changeSet.operation !== ResourceOperation.NOOP) {
69
69
  throw new ApplyValidationError(plan, validationPlan);
70
70
  }
@@ -3,7 +3,7 @@ import { ParameterChange } from './change-set.js';
3
3
  import { Plan } from './plan.js';
4
4
  import { StatefulParameter } from './stateful-parameter.js';
5
5
  import { ResourceParameterOptions, ValidationResult } from './resource-types.js';
6
- import { ParameterOptions } from './plan-types.js';
6
+ import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions } from './plan-types.js';
7
7
  import { TransformParameter } from './transform-parameter.js';
8
8
  import { ResourceOptions } from './resource-options.js';
9
9
  import Ajv from 'ajv';
@@ -24,7 +24,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
24
24
  protected constructor(options: ResourceOptions<T>);
25
25
  onInitialize(): Promise<void>;
26
26
  validateResource(parameters: unknown): Promise<ValidationResult>;
27
- plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
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;
30
30
  private _applyModify;
@@ -32,11 +32,12 @@ export declare abstract class Resource<T extends StringIndexedObject> {
32
32
  private validateRefreshResults;
33
33
  private applyTransformParameters;
34
34
  private addDefaultValues;
35
- private refreshResourceParameters;
35
+ private refreshNonStatefulParameters;
36
36
  private refreshStatefulParameters;
37
+ private validatePlanInputs;
37
38
  validate(parameters: unknown): Promise<ValidationResult>;
38
- abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
39
- abstract applyCreate(plan: Plan<T>): Promise<void>;
40
- applyModify(pc: ParameterChange<T>, plan: Plan<T>): Promise<void>;
41
- abstract applyDestroy(plan: Plan<T>): Promise<void>;
39
+ abstract refresh(values: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
40
+ abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
41
+ applyModify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void>;
42
+ abstract applyDestroy(plan: DestroyPlan<T>): Promise<void>;
42
43
  }
@@ -49,21 +49,22 @@ export class Resource {
49
49
  }
50
50
  return this.validate(parameters);
51
51
  }
52
- async plan(desiredConfig) {
52
+ async plan(desiredConfig, currentConfig = null, statefulMode = false) {
53
+ this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
53
54
  const planOptions = {
54
- statefulMode: false,
55
+ statefulMode,
55
56
  parameterOptions: this.parameterOptions,
56
57
  };
57
58
  this.addDefaultValues(desiredConfig);
58
- const parsedConfig = new ConfigParser(desiredConfig, this.statefulParameters, this.transformParameters);
59
- const { parameters: desiredParameters, resourceMetadata, resourceParameters, statefulParameters, transformParameters, } = parsedConfig;
60
- await this.applyTransformParameters(transformParameters, resourceParameters);
61
- const currentParameters = await this.refreshResourceParameters(resourceParameters);
59
+ await this.applyTransformParameters(desiredConfig);
60
+ const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters);
61
+ const { desiredParameters, resourceMetadata, nonStatefulParameters, statefulParameters, } = parsedConfig;
62
+ const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
62
63
  if (currentParameters == null) {
63
- return Plan.create({ ...resourceParameters, ...statefulParameters }, null, resourceMetadata, planOptions);
64
+ return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
64
65
  }
65
66
  const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
66
- return Plan.create({ ...resourceParameters, ...statefulParameters }, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
67
+ return Plan.create(desiredParameters, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
67
68
  }
68
69
  async apply(plan) {
69
70
  if (plan.getResourceType() !== this.typeId) {
@@ -151,20 +152,30 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
151
152
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
152
153
  }
153
154
  }
154
- async applyTransformParameters(transformParameters, desired) {
155
- const orderedEntries = [...Object.entries(transformParameters)]
155
+ async applyTransformParameters(desired) {
156
+ if (!desired) {
157
+ return;
158
+ }
159
+ const transformParameters = [...this.transformParameters.entries()]
156
160
  .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
157
- for (const [key, value] of orderedEntries) {
158
- const transformedValue = await this.transformParameters.get(key).transform(value);
161
+ for (const [key, transformParameter] of transformParameters) {
162
+ if (desired[key] === undefined) {
163
+ continue;
164
+ }
165
+ const transformedValue = await transformParameter.transform(desired[key]);
159
166
  if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
160
167
  throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
161
168
  }
169
+ delete desired[key];
162
170
  Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
163
171
  desired[tvKey] = tvValue;
164
172
  });
165
173
  }
166
174
  }
167
175
  addDefaultValues(desired) {
176
+ if (!desired) {
177
+ return;
178
+ }
168
179
  Object.entries(this.defaultValues)
169
180
  .forEach(([key, defaultValue]) => {
170
181
  if (defaultValue !== undefined && desired[key] === undefined) {
@@ -172,7 +183,7 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
172
183
  }
173
184
  });
174
185
  }
175
- async refreshResourceParameters(resourceParameters) {
186
+ async refreshNonStatefulParameters(resourceParameters) {
176
187
  const entriesToRefresh = new Map(Object.entries(resourceParameters));
177
188
  const currentParameters = await this.refresh(entriesToRefresh);
178
189
  this.validateRefreshResults(currentParameters, entriesToRefresh);
@@ -204,6 +215,14 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
204
215
  }
205
216
  return currentParameters;
206
217
  }
218
+ validatePlanInputs(desired, current, statefulMode) {
219
+ if (!desired && !current) {
220
+ throw new Error('Desired config and current config cannot both be missing');
221
+ }
222
+ if (!statefulMode && !desired) {
223
+ throw new Error('Desired config must be provided in non-stateful mode');
224
+ }
225
+ }
207
226
  async validate(parameters) {
208
227
  return {
209
228
  isValid: true,
@@ -214,23 +233,46 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
214
233
  ;
215
234
  }
216
235
  class ConfigParser {
217
- config;
236
+ desiredConfig;
237
+ currentConfig;
218
238
  statefulParametersMap;
219
239
  transformParametersMap;
220
- constructor(config, statefulParameters, transformParameters) {
221
- this.config = config;
240
+ constructor(desiredConfig, currentConfig, statefulParameters, transformParameters) {
241
+ this.desiredConfig = desiredConfig;
242
+ this.currentConfig = currentConfig;
222
243
  this.statefulParametersMap = statefulParameters;
223
244
  this.transformParametersMap = transformParameters;
224
245
  }
225
246
  get resourceMetadata() {
226
- const { resourceMetadata } = splitUserConfig(this.config);
227
- return resourceMetadata;
247
+ const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
248
+ const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
249
+ if (!desiredMetadata && !currentMetadata) {
250
+ throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`);
251
+ }
252
+ if (currentMetadata && desiredMetadata && (Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
253
+ || Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value))) {
254
+ throw new Error(`The metadata for the current config does not match the desired config.
255
+ Desired metadata:
256
+ ${JSON.stringify(desiredMetadata, null, 2)}
257
+
258
+ Current metadata:
259
+ ${JSON.stringify(currentMetadata, null, 2)}`);
260
+ }
261
+ return desiredMetadata ?? currentMetadata;
228
262
  }
229
- get parameters() {
230
- const { parameters } = splitUserConfig(this.config);
263
+ get desiredParameters() {
264
+ if (!this.desiredConfig) {
265
+ return null;
266
+ }
267
+ const { parameters } = splitUserConfig(this.desiredConfig);
231
268
  return parameters;
232
269
  }
233
- get resourceParameters() {
270
+ get parameters() {
271
+ const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
272
+ const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
273
+ return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) };
274
+ }
275
+ get nonStatefulParameters() {
234
276
  const parameters = this.parameters;
235
277
  return Object.fromEntries([
236
278
  ...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
@@ -242,10 +284,4 @@ class ConfigParser {
242
284
  ...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
243
285
  ]);
244
286
  }
245
- get transformParameters() {
246
- const parameters = this.parameters;
247
- return Object.fromEntries([
248
- ...Object.entries(parameters).filter(([key]) => this.transformParametersMap.has(key)),
249
- ]);
250
- }
251
287
  }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,5 @@ export * from './entities/plan.js';
8
8
  export * from './entities/plan-types.js';
9
9
  export * from './entities/stateful-parameter.js';
10
10
  export * from './entities/errors.js';
11
- export * from './utils/test-utils.js';
12
11
  export * from './utils/utils.js';
13
12
  export declare function runPlugin(plugin: Plugin): Promise<void>;
package/dist/index.js CHANGED
@@ -8,7 +8,6 @@ export * from './entities/plan.js';
8
8
  export * from './entities/plan-types.js';
9
9
  export * from './entities/stateful-parameter.js';
10
10
  export * from './entities/errors.js';
11
- export * from './utils/test-utils.js';
12
11
  export * from './utils/utils.js';
13
12
  export async function runPlugin(plugin) {
14
13
  const messageHandler = new MessageHandler(plugin);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.65",
3
+ "version": "1.0.67",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -123,8 +123,8 @@ export class ChangeSet<T extends StringIndexedObject> {
123
123
  ): ParameterChange<T>[] {
124
124
  const parameterChangeSet = new Array<ParameterChange<T>>();
125
125
 
126
- const _desired = { ...desired };
127
- const _current = { ...current };
126
+ const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
127
+ const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
128
128
 
129
129
  this.addDefaultValues(_desired, parameterOptions);
130
130
 
@@ -190,8 +190,9 @@ export class ChangeSet<T extends StringIndexedObject> {
190
190
  ): ParameterChange<T>[] {
191
191
  const parameterChangeSet = new Array<ParameterChange<T>>();
192
192
 
193
- const _desired = { ...desired };
194
- const _current = { ...current };
193
+ const _desired = Object.fromEntries(Object.entries(desired ?? {}).filter(([, v]) => v != null));
194
+ const _current = Object.fromEntries(Object.entries(current ?? {}).filter(([, v]) => v != null));
195
+
195
196
 
196
197
  this.addDefaultValues(_desired, parameterOptions);
197
198
 
@@ -1,3 +1,6 @@
1
+ import { Plan } from './plan.js';
2
+ import { StringIndexedObject } from 'codify-schemas';
3
+
1
4
  /**
2
5
  * Customize properties for specific parameters. This will alter the way the library process changes to the parameter.
3
6
  */
@@ -24,3 +27,8 @@ export interface PlanOptions<T> {
24
27
  statefulMode: boolean;
25
28
  parameterOptions?: Record<keyof T, ParameterOptions>;
26
29
  }
30
+
31
+ export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
32
+ export type CreatePlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'desiredConfig'>
33
+ export type DestroyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig'>
34
+ export type ModifyPlan<T extends StringIndexedObject> = WithRequired<Plan<T>, 'currentConfig' | 'desiredConfig'>
@@ -19,11 +19,7 @@ describe('Plan entity tests', () => {
19
19
  }]
20
20
  }, resource.defaultValues);
21
21
 
22
- expect(plan.currentConfig).toMatchObject({
23
- type: 'type',
24
- propA: null,
25
- propB: null,
26
- })
22
+ expect(plan.currentConfig).to.be.null;
27
23
 
28
24
  expect(plan.desiredConfig).toMatchObject({
29
25
  type: 'type',
@@ -56,11 +52,7 @@ describe('Plan entity tests', () => {
56
52
  propB: 'propBValue',
57
53
  })
58
54
 
59
- expect(plan.desiredConfig).toMatchObject({
60
- type: 'type',
61
- propA: null,
62
- propB: null,
63
- })
55
+ expect(plan.desiredConfig).to.be.null;
64
56
 
65
57
  expect(plan.changeSet.parameterChanges
66
58
  .every((pc) => pc.operation === ParameterOperation.REMOVE)
@@ -117,11 +109,7 @@ describe('Plan entity tests', () => {
117
109
  }]
118
110
  }, resource.defaultValues);
119
111
 
120
- expect(plan.currentConfig).toMatchObject({
121
- type: 'type',
122
- propA: null,
123
- propB: null,
124
- })
112
+ expect(plan.currentConfig).to.be.null
125
113
 
126
114
  expect(plan.desiredConfig).toMatchObject({
127
115
  type: 'type',
@@ -144,14 +144,22 @@ export class Plan<T extends StringIndexedObject> {
144
144
 
145
145
  }
146
146
 
147
- get desiredConfig(): T {
147
+ get desiredConfig(): T | null {
148
+ if (this.changeSet.operation === ResourceOperation.DESTROY) {
149
+ return null;
150
+ }
151
+
148
152
  return {
149
153
  ...this.resourceMetadata,
150
154
  ...this.changeSet.desiredParameters,
151
155
  }
152
156
  }
153
157
 
154
- get currentConfig(): T {
158
+ get currentConfig(): T | null {
159
+ if (this.changeSet.operation === ResourceOperation.CREATE) {
160
+ return null;
161
+ }
162
+
155
163
  return {
156
164
  ...this.resourceMetadata,
157
165
  ...this.changeSet.currentParameters,
@@ -44,7 +44,7 @@ class TestResource extends Resource<TestConfig> {
44
44
 
45
45
  describe('Plugin tests', () => {
46
46
  it('Validates that applies were successfully applied', async () => {
47
- const resource = new class extends TestResource {
47
+ const resource= new class extends TestResource {
48
48
  async applyCreate(plan: Plan<TestConfig>): Promise<void> {
49
49
  }
50
50
 
@@ -56,9 +56,9 @@ describe('Plugin tests', () => {
56
56
  }
57
57
  }
58
58
 
59
- const testPlugin = Plugin.create('testPlugin', [resource])
59
+ const plugin = Plugin.create('testPlugin', [resource])
60
60
 
61
- const desiredPlan = {
61
+ const plan = {
62
62
  operation: ResourceOperation.CREATE,
63
63
  resourceType: 'testResource',
64
64
  parameters: [
@@ -67,7 +67,7 @@ describe('Plugin tests', () => {
67
67
  };
68
68
 
69
69
  // If this doesn't throw then it passes the test
70
- await testPlugin.apply({ plan: desiredPlan });
70
+ await plugin.apply({ plan });
71
71
  });
72
72
 
73
73
  it('Validates that applies were successfully applied (error)', async () => {
@@ -80,10 +80,9 @@ describe('Plugin tests', () => {
80
80
  return null;
81
81
  }
82
82
  }
83
+ const plugin = Plugin.create('testPlugin', [resource])
83
84
 
84
- const testPlugin = Plugin.create('testPlugin', [resource])
85
-
86
- const desiredPlan = {
85
+ const plan = {
87
86
  operation: ResourceOperation.CREATE,
88
87
  resourceType: 'testResource',
89
88
  parameters: [
@@ -91,6 +90,104 @@ describe('Plugin tests', () => {
91
90
  ]
92
91
  };
93
92
 
94
- await expect(async () => testPlugin.apply({ plan: desiredPlan })).rejects.toThrowError(expect.any(ApplyValidationError));
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])
108
+
109
+ const plan = {
110
+ operation: ResourceOperation.DESTROY,
111
+ resourceType: 'testResource',
112
+ parameters: [
113
+ { name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' },
114
+ ]
115
+ };
116
+
117
+ // If this doesn't throw then it passes the test
118
+ await testPlugin.apply({ plan })
119
+ });
120
+
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])
157
+
158
+ const plan = {
159
+ operation: ResourceOperation.RECREATE,
160
+ resourceType: 'testResource',
161
+ parameters: [
162
+ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' },
163
+ ]
164
+ };
165
+
166
+ // If this doesn't throw then it passes the test
167
+ await testPlugin.apply({ plan })
168
+ });
169
+
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])
181
+
182
+ const plan = {
183
+ operation: ResourceOperation.DESTROY,
184
+ resourceType: 'testResource',
185
+ parameters: [
186
+ { name: 'propA', operation: ParameterOperation.REMOVE, newValue: 'def', previousValue: 'abc' },
187
+ ]
188
+ };
189
+
190
+ // If this doesn't throw then it passes the test
191
+ expect(async () => await testPlugin.apply({ plan })).rejects.toThrowError(expect.any(ApplyValidationError));
95
192
  });
96
193
  });
@@ -95,7 +95,11 @@ export class Plugin {
95
95
 
96
96
  // Perform a validation check after to ensure that the plan was properly applied.
97
97
  // Sometimes no errors are returned (exit code 0) but the apply was not successful
98
- const validationPlan = await resource.plan({ ...plan.resourceMetadata, ...plan.desiredConfig });
98
+ const validationPlan = await resource.plan(
99
+ plan.desiredConfig,
100
+ plan.currentConfig,
101
+ true,
102
+ );
99
103
  if (validationPlan.changeSet.operation !== ResourceOperation.NOOP) {
100
104
  throw new ApplyValidationError(plan, validationPlan);
101
105
  }
@@ -6,7 +6,7 @@ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
6
6
  import { TestConfig, TestResource } from './resource.test.js';
7
7
  import { TransformParameter } from './transform-parameter.js';
8
8
 
9
- class TestParameter extends StatefulParameter<TestConfig, string> {
9
+ export class TestParameter extends StatefulParameter<TestConfig, string> {
10
10
  constructor(options?: StatefulParameterOptions<string>) {
11
11
  super(options ?? {})
12
12
  }
@@ -55,11 +55,7 @@ describe('Resource parameter tests', () => {
55
55
  })
56
56
 
57
57
  expect(statefulParameter.refresh.notCalled).to.be.true;
58
- expect(plan.currentConfig).toMatchObject({
59
- type: 'resource',
60
- propA: null,
61
- propB: null,
62
- })
58
+ expect(plan.currentConfig).to.be.null;
63
59
  expect(plan.desiredConfig).toMatchObject({
64
60
  type: 'resource',
65
61
  propA: 'a',
@@ -171,10 +167,7 @@ describe('Resource parameter tests', () => {
171
167
  })
172
168
 
173
169
  expect(statefulParameter.refresh.notCalled).to.be.true;
174
- expect(plan.currentConfig).toMatchObject({
175
- type: 'resource',
176
- propA: null,
177
- })
170
+ expect(plan.currentConfig).to.be.null;
178
171
  expect(plan.desiredConfig).toMatchObject({
179
172
  type: 'resource',
180
173
  propA: 'abc',
@@ -182,7 +175,6 @@ describe('Resource parameter tests', () => {
182
175
  expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
183
176
  })
184
177
 
185
-
186
178
  it('Filters array results in stateless mode to prevent modify from being called', async () => {
187
179
  const statefulParameter = new class extends TestParameter {
188
180
  async refresh(): Promise<any | null> {
@@ -350,10 +342,10 @@ describe('Resource parameter tests', () => {
350
342
  propE: 'propE',
351
343
  });
352
344
 
353
- expect(plan.currentConfig.propB).to.be.lessThan(plan.currentConfig.propC as any);
354
- expect(plan.currentConfig.propC).to.be.lessThan(plan.currentConfig.propA as any);
355
- expect(plan.currentConfig.propA).to.be.lessThan(plan.currentConfig.propD as any);
356
- expect(plan.currentConfig.propD).to.be.lessThan(plan.currentConfig.propE as any);
345
+ expect(plan.currentConfig?.propB).to.be.lessThan(plan.currentConfig?.propC as any);
346
+ expect(plan.currentConfig?.propC).to.be.lessThan(plan.currentConfig?.propA as any);
347
+ expect(plan.currentConfig?.propA).to.be.lessThan(plan.currentConfig?.propD as any);
348
+ expect(plan.currentConfig?.propD).to.be.lessThan(plan.currentConfig?.propE as any);
357
349
  })
358
350
 
359
351
  it('Applies stateful parameters in the order specified', async () => {
@@ -483,9 +475,9 @@ describe('Resource parameter tests', () => {
483
475
  expect(resource.refresh.getCall(0).firstArg.has('propB')).to.be.true;
484
476
  expect(resource.refresh.getCall(0).firstArg.has('propC')).to.be.false;
485
477
 
486
- expect(plan.desiredConfig.propA).to.eq('propA');
487
- expect(plan.desiredConfig.propB).to.eq(10);
488
- expect(plan.desiredConfig.propC).to.be.undefined;
478
+ expect(plan.desiredConfig?.propA).to.eq('propA');
479
+ expect(plan.desiredConfig?.propB).to.eq(10);
480
+ expect(plan.desiredConfig?.propC).to.be.undefined;
489
481
 
490
482
  expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
491
483
  })
@@ -570,8 +562,8 @@ describe('Resource parameter tests', () => {
570
562
  propC: 'propC',
571
563
  });
572
564
 
573
- expect(plan.desiredConfig.propE).to.be.lessThan(plan.desiredConfig.propF as any);
574
- expect(plan.desiredConfig.propF).to.be.lessThan(plan.desiredConfig.propD as any);
565
+ expect(plan.desiredConfig?.propE).to.be.lessThan(plan.desiredConfig?.propF as any);
566
+ expect(plan.desiredConfig?.propF).to.be.lessThan(plan.desiredConfig?.propD as any);
575
567
  })
576
568
 
577
569
  it('Plans transform even for creating new resources', async () => {
@@ -602,11 +594,7 @@ describe('Resource parameter tests', () => {
602
594
  propB: 10,
603
595
  propC: 'propC',
604
596
  });
605
- expect(plan.currentConfig).toMatchObject({
606
- type: 'resourceType',
607
- propD: null,
608
- propE: null,
609
- })
597
+ expect(plan.currentConfig).to.be.null;
610
598
  expect(plan.desiredConfig).toMatchObject({
611
599
  type: 'resourceType',
612
600
  propD: 'abc',