codify-plugin-lib 1.0.65 → 1.0.66

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) {
@@ -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
  }
@@ -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,8 +32,9 @@ 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
39
  abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
39
40
  abstract applyCreate(plan: Plan<T>): Promise<void>;
@@ -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.66",
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
 
@@ -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',
@@ -0,0 +1,247 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { TestConfig, TestResource } from './resource.test.js';
3
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
4
+ import { TestParameter } from './resource-parameters.test.js';
5
+ import { StatefulParameter } from './stateful-parameter.js';
6
+
7
+
8
+ describe('Resource tests for stateful plans', () => {
9
+ it('Supports delete operations ', async () => {
10
+ const resource = new class extends TestResource {
11
+ constructor() {
12
+ super({ type: 'resource' });
13
+ }
14
+
15
+ async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
16
+ return {
17
+ propA: 'propADifferent',
18
+ propB: undefined,
19
+ propC: 'propCDifferent',
20
+ }
21
+ }
22
+ }
23
+
24
+ const plan = await resource.plan(
25
+ null,
26
+ {
27
+ type: 'resource',
28
+ propA: 'propA',
29
+ propB: 10,
30
+ propC: 'propC',
31
+ }, true
32
+ );
33
+
34
+ expect(plan).toMatchObject({
35
+ changeSet: {
36
+ operation: ResourceOperation.DESTROY,
37
+ parameterChanges: [
38
+ {
39
+ name: "propA",
40
+ previousValue: "propADifferent",
41
+ newValue: null,
42
+ operation: ParameterOperation.REMOVE
43
+ },
44
+ {
45
+ name: "propC",
46
+ previousValue: "propCDifferent",
47
+ newValue: null,
48
+ operation: ParameterOperation.REMOVE
49
+ },
50
+ ]
51
+ },
52
+ resourceMetadata: {
53
+ type: 'resource'
54
+ }
55
+ })
56
+ })
57
+
58
+ it('Supports create operations', async () => {
59
+ const resource = new class extends TestResource {
60
+ constructor() {
61
+ super({ type: 'resource' });
62
+ }
63
+
64
+ async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ const plan = await resource.plan(
70
+ {
71
+ type: 'resource',
72
+ propA: 'propA',
73
+ propB: 10,
74
+ propC: 'propC',
75
+ },
76
+ null,
77
+ true
78
+ );
79
+
80
+ expect(plan).toMatchObject({
81
+ changeSet: {
82
+ operation: ResourceOperation.CREATE,
83
+ parameterChanges: [
84
+ {
85
+ name: "propA",
86
+ newValue: "propA",
87
+ previousValue: null,
88
+ operation: ParameterOperation.ADD
89
+ },
90
+ {
91
+ name: "propB",
92
+ newValue: 10,
93
+ previousValue: null,
94
+ operation: ParameterOperation.ADD
95
+ },
96
+ {
97
+ name: "propC",
98
+ newValue: 'propC',
99
+ previousValue: null,
100
+ operation: ParameterOperation.ADD
101
+ },
102
+ ]
103
+ },
104
+ resourceMetadata: {
105
+ type: 'resource'
106
+ }
107
+ })
108
+ })
109
+
110
+ it('Supports re-create operations', async () => {
111
+ const resource = new class extends TestResource {
112
+ constructor() {
113
+ super({ type: 'resource' });
114
+ }
115
+
116
+ async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
117
+ return {
118
+ propA: 'propA',
119
+ propC: 'propC',
120
+ propB: undefined
121
+ };
122
+ }
123
+ }
124
+
125
+ const plan = await resource.plan(
126
+ {
127
+ type: 'resource',
128
+ propA: 'propA',
129
+ propB: 10,
130
+ propC: 'propC',
131
+ },
132
+ {
133
+ type: 'resource',
134
+ propA: 'propA',
135
+ propC: 'propC'
136
+ },
137
+ true
138
+ );
139
+
140
+ expect(plan).toMatchObject({
141
+ changeSet: {
142
+ operation: ResourceOperation.RECREATE,
143
+ parameterChanges: expect.arrayContaining([
144
+ {
145
+ name: "propA",
146
+ newValue: "propA",
147
+ previousValue: "propA",
148
+ operation: ParameterOperation.NOOP
149
+ },
150
+ {
151
+ name: "propB",
152
+ newValue: 10,
153
+ previousValue: null,
154
+ operation: ParameterOperation.ADD
155
+ },
156
+ {
157
+ name: "propC",
158
+ newValue: 'propC',
159
+ previousValue: 'propC',
160
+ operation: ParameterOperation.NOOP
161
+ },
162
+ ])
163
+ },
164
+ resourceMetadata: {
165
+ type: 'resource'
166
+ }
167
+ })
168
+ })
169
+
170
+ it('Supports stateful parameters', async () => {
171
+ const statefulParameter = new class extends TestParameter {
172
+ async refresh(): Promise<string | null> {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ const resource = new class extends TestResource {
178
+ constructor() {
179
+ super({
180
+ type: 'resource',
181
+ parameterOptions: {
182
+ propD: { statefulParameter: statefulParameter as StatefulParameter<TestConfig, string> },
183
+ }
184
+ });
185
+ }
186
+
187
+ async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
188
+ return {
189
+ propA: 'propA',
190
+ propC: 'propC',
191
+ propB: undefined
192
+ };
193
+ }
194
+ }
195
+
196
+ const plan = await resource.plan(
197
+ {
198
+ type: 'resource',
199
+ propA: 'propA',
200
+ propB: 10,
201
+ propC: 'propC',
202
+ propD: 'propD'
203
+ },
204
+ {
205
+ type: 'resource',
206
+ propA: 'propA',
207
+ propC: 'propC'
208
+ },
209
+ true
210
+ );
211
+
212
+ expect(plan).toMatchObject({
213
+ changeSet: {
214
+ operation: ResourceOperation.RECREATE,
215
+ parameterChanges: expect.arrayContaining([
216
+ {
217
+ name: "propA",
218
+ newValue: "propA",
219
+ previousValue: "propA",
220
+ operation: ParameterOperation.NOOP
221
+ },
222
+ {
223
+ name: "propB",
224
+ newValue: 10,
225
+ previousValue: null,
226
+ operation: ParameterOperation.ADD
227
+ },
228
+ {
229
+ name: "propC",
230
+ newValue: 'propC',
231
+ previousValue: 'propC',
232
+ operation: ParameterOperation.NOOP
233
+ },
234
+ {
235
+ name: "propD",
236
+ newValue: 'propD',
237
+ previousValue: null,
238
+ operation: ParameterOperation.ADD
239
+ },
240
+ ])
241
+ },
242
+ resourceMetadata: {
243
+ type: 'resource'
244
+ }
245
+ })
246
+ })
247
+ })
@@ -301,8 +301,8 @@ describe('Resource tests', () => {
301
301
  }
302
302
 
303
303
  const plan = await resource.plan({ type: 'resource'})
304
- expect(plan.currentConfig.propA).to.eq('propAAfter');
305
- expect(plan.desiredConfig.propA).to.eq('propADefault');
304
+ expect(plan.currentConfig?.propA).to.eq('propAAfter');
305
+ expect(plan.desiredConfig?.propA).to.eq('propADefault');
306
306
  expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
307
307
  })
308
308
 
@@ -327,8 +327,8 @@ describe('Resource tests', () => {
327
327
  }
328
328
 
329
329
  const plan = await resource.plan({ type: 'resource'})
330
- expect(plan.currentConfig.propE).to.eq('propEDefault');
331
- expect(plan.desiredConfig.propE).to.eq('propEDefault');
330
+ expect(plan.currentConfig?.propE).to.eq('propEDefault');
331
+ expect(plan.desiredConfig?.propE).to.eq('propEDefault');
332
332
  expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
333
333
  })
334
334
 
@@ -349,8 +349,8 @@ describe('Resource tests', () => {
349
349
  }
350
350
 
351
351
  const plan = await resource.plan({ type: 'resource'})
352
- expect(plan.currentConfig.propE).to.eq(null);
353
- expect(plan.desiredConfig.propE).to.eq('propEDefault');
352
+ expect(plan.currentConfig).to.be.null
353
+ expect(plan.desiredConfig!.propE).to.eq('propEDefault');
354
354
  expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
355
355
  })
356
356
 
@@ -377,8 +377,8 @@ describe('Resource tests', () => {
377
377
  }
378
378
 
379
379
  const plan = await resource.plan({ type: 'resource', propA: 'propA'})
380
- expect(plan.currentConfig.propA).to.eq('propAAfter');
381
- expect(plan.desiredConfig.propA).to.eq('propA');
380
+ expect(plan.currentConfig?.propA).to.eq('propAAfter');
381
+ expect(plan.desiredConfig?.propA).to.eq('propA');
382
382
  expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
383
383
  });
384
384
 
@@ -74,38 +74,39 @@ export abstract class Resource<T extends StringIndexedObject> {
74
74
  return this.validate(parameters);
75
75
  }
76
76
 
77
- // TODO: Add state in later.
78
- // Currently only calculating how to add things to reach desired state. Can't delete resources.
79
- // Add previousConfig as a parameter for plan(desired, previous);
80
- async plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>> {
77
+ // TODO: Currently stateful mode expects that the currentConfig does not need any additional transformations (default and transform parameters)
78
+ // This may change in the future?
79
+ async plan(
80
+ desiredConfig: Partial<T> & ResourceConfig | null,
81
+ currentConfig: Partial<T> & ResourceConfig | null = null,
82
+ statefulMode = false,
83
+ ): Promise<Plan<T>> {
84
+ this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
85
+
81
86
  const planOptions: PlanOptions<T> = {
82
- statefulMode: false,
87
+ statefulMode,
83
88
  parameterOptions: this.parameterOptions,
84
89
  }
85
90
 
86
91
  this.addDefaultValues(desiredConfig);
92
+ await this.applyTransformParameters(desiredConfig);
87
93
 
88
94
  // Parse data from the user supplied config
89
- const parsedConfig = new ConfigParser(desiredConfig, this.statefulParameters, this.transformParameters)
95
+ const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters)
90
96
  const {
91
- parameters: desiredParameters,
97
+ desiredParameters,
92
98
  resourceMetadata,
93
- resourceParameters,
99
+ nonStatefulParameters,
94
100
  statefulParameters,
95
- transformParameters,
96
101
  } = parsedConfig;
97
102
 
98
- // Apply transform parameters. Transform parameters turn into other parameters.
99
- // Ex: csvFile: './location' => { password: 'pass', 'username': 'user' }
100
- await this.applyTransformParameters(transformParameters, resourceParameters);
101
-
102
103
  // Refresh resource parameters. This refreshes the parameters that configure the resource itself
103
- const currentParameters = await this.refreshResourceParameters(resourceParameters);
104
+ const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
104
105
 
105
106
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
106
107
  if (currentParameters == null) {
107
108
  return Plan.create(
108
- { ...resourceParameters, ...statefulParameters },
109
+ desiredParameters,
109
110
  null,
110
111
  resourceMetadata,
111
112
  planOptions,
@@ -116,7 +117,7 @@ export abstract class Resource<T extends StringIndexedObject> {
116
117
  const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
117
118
 
118
119
  return Plan.create(
119
- { ...resourceParameters, ...statefulParameters },
120
+ desiredParameters,
120
121
  { ...currentParameters, ...statefulCurrentParameters } as Partial<T>,
121
122
  resourceMetadata,
122
123
  planOptions,
@@ -232,17 +233,29 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
232
233
  }
233
234
  }
234
235
 
235
- private async applyTransformParameters(transformParameters: Partial<T>, desired: Partial<T>): Promise<void> {
236
- const orderedEntries = [...Object.entries(transformParameters)]
236
+ private async applyTransformParameters(desired: Partial<T> | null): Promise<void> {
237
+ if (!desired) {
238
+ return;
239
+ }
240
+
241
+ const transformParameters = [...this.transformParameters.entries()]
237
242
  .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
238
243
 
239
- for (const [key, value] of orderedEntries) {
240
- const transformedValue = await this.transformParameters.get(key)!.transform(value);
244
+ for (const [key, transformParameter] of transformParameters) {
245
+ if (desired[key] === undefined) {
246
+ continue;
247
+ }
248
+
249
+ const transformedValue = await transformParameter.transform(desired[key]);
241
250
 
242
251
  if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
243
252
  throw new Error(`Transform parameter ${key as string} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
244
253
  }
245
254
 
255
+ // Remove original transform parameter from the config
256
+ delete desired[key];
257
+
258
+ // Add the new transformed values
246
259
  Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
247
260
  // @ts-ignore
248
261
  desired[tvKey] = tvValue;
@@ -250,7 +263,11 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
250
263
  }
251
264
  }
252
265
 
253
- private addDefaultValues(desired: Partial<T>): void {
266
+ private addDefaultValues(desired: Partial<T> | null): void {
267
+ if (!desired) {
268
+ return;
269
+ }
270
+
254
271
  Object.entries(this.defaultValues)
255
272
  .forEach(([key, defaultValue]) => {
256
273
  if (defaultValue !== undefined && desired[key as any] === undefined) {
@@ -260,10 +277,11 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
260
277
  });
261
278
  }
262
279
 
263
- private async refreshResourceParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
264
- const entriesToRefresh = new Map(Object.entries(resourceParameters));
280
+ private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Partial<T> | null> {
281
+ const entriesToRefresh = new Map<keyof T, T[keyof T]>(
282
+ Object.entries(resourceParameters)
283
+ )
265
284
  const currentParameters = await this.refresh(entriesToRefresh);
266
-
267
285
  this.validateRefreshResults(currentParameters, entriesToRefresh);
268
286
  return currentParameters;
269
287
  }
@@ -308,6 +326,20 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
308
326
  return currentParameters;
309
327
  }
310
328
 
329
+ private validatePlanInputs(
330
+ desired: Partial<T> & ResourceConfig | null,
331
+ current: Partial<T> & ResourceConfig | null,
332
+ statefulMode: boolean,
333
+ ) {
334
+ if (!desired && !current) {
335
+ throw new Error('Desired config and current config cannot both be missing')
336
+ }
337
+
338
+ if (!statefulMode && !desired) {
339
+ throw new Error('Desired config must be provided in non-stateful mode')
340
+ }
341
+ }
342
+
311
343
  async validate(parameters: unknown): Promise<ValidationResult> {
312
344
  return {
313
345
  isValid: true,
@@ -324,51 +356,76 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
324
356
  }
325
357
 
326
358
  class ConfigParser<T extends StringIndexedObject> {
327
- private config: Partial<T> & ResourceConfig;
359
+ private desiredConfig: Partial<T> & ResourceConfig | null;
360
+ private currentConfig: Partial<T> & ResourceConfig | null;
328
361
  private statefulParametersMap: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
329
362
  private transformParametersMap: Map<keyof T, TransformParameter<T>>;
330
363
 
331
364
  constructor(
332
- config: Partial<T> & ResourceConfig,
365
+ desiredConfig: Partial<T> & ResourceConfig | null,
366
+ currentConfig: Partial<T> & ResourceConfig | null,
333
367
  statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>,
334
- transformParameters: Map<keyof T, TransformParameter<T>>,
368
+ transformParameters: Map<keyof T, TransformParameter<T>>,
335
369
  ) {
336
- this.config = config;
370
+ this.desiredConfig = desiredConfig;
371
+ this.currentConfig = currentConfig
337
372
  this.statefulParametersMap = statefulParameters;
338
373
  this.transformParametersMap = transformParameters;
339
374
  }
340
375
 
341
376
  get resourceMetadata(): ResourceConfig {
342
- const { resourceMetadata } = splitUserConfig(this.config);
343
- return resourceMetadata;
377
+ const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
378
+ const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
379
+
380
+ if (!desiredMetadata && !currentMetadata) {
381
+ throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`)
382
+ }
383
+
384
+ if (currentMetadata && desiredMetadata && (
385
+ Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
386
+ || Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value)
387
+ )) {
388
+ throw new Error(`The metadata for the current config does not match the desired config.
389
+ Desired metadata:
390
+ ${JSON.stringify(desiredMetadata, null, 2)}
391
+
392
+ Current metadata:
393
+ ${JSON.stringify(currentMetadata, null, 2)}`);
394
+ }
395
+
396
+ return desiredMetadata ?? currentMetadata!;
344
397
  }
345
398
 
346
- get parameters(): Partial<T> {
347
- const { parameters } = splitUserConfig(this.config);
399
+ get desiredParameters(): Partial<T> | null {
400
+ if (!this.desiredConfig) {
401
+ return null;
402
+ }
403
+
404
+ const { parameters } = splitUserConfig(this.desiredConfig);
348
405
  return parameters;
349
406
  }
350
407
 
351
- get resourceParameters(): Partial<T> {
352
- const parameters = this.parameters;
353
408
 
354
- return Object.fromEntries([
355
- ...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
356
- ]) as Partial<T>;
409
+ get parameters(): Partial<T> {
410
+ const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
411
+ const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
412
+
413
+ return { ...(desiredParameters ?? {}), ...(currentParameters ?? {}) } as Partial<T>;
357
414
  }
358
415
 
359
- get statefulParameters(): Partial<T> {
416
+ get nonStatefulParameters(): Partial<T> {
360
417
  const parameters = this.parameters;
361
418
 
362
419
  return Object.fromEntries([
363
- ...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
420
+ ...Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))),
364
421
  ]) as Partial<T>;
365
422
  }
366
423
 
367
- get transformParameters(): Partial<T> {
424
+ get statefulParameters(): Partial<T> {
368
425
  const parameters = this.parameters;
369
426
 
370
427
  return Object.fromEntries([
371
- ...Object.entries(parameters).filter(([key]) => this.transformParametersMap.has(key)),
428
+ ...Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)),
372
429
  ]) as Partial<T>;
373
430
  }
374
431
  }
@@ -10,10 +10,8 @@ interface TestConfig {
10
10
  }
11
11
 
12
12
  class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
13
- constructor(options?: ArrayStatefulParameterOptions<TestConfig>) {
14
- super(options ?? {
15
- name: 'propA'
16
- })
13
+ constructor(options?: ArrayStatefulParameterOptions<string>) {
14
+ super(options)
17
15
  }
18
16
 
19
17
  async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
@@ -104,7 +102,6 @@ describe('Stateful parameter tests', () => {
104
102
  const testParameter = spy(new class extends TestArrayParameter {
105
103
  constructor() {
106
104
  super({
107
- name: 'propA',
108
105
  isElementEqual: (desired, current) => current.includes(desired),
109
106
  });
110
107
  }
package/src/index.ts CHANGED
@@ -11,7 +11,6 @@ export * from './entities/plan-types.js'
11
11
  export * from './entities/stateful-parameter.js'
12
12
  export * from './entities/errors.js'
13
13
 
14
- export * from './utils/test-utils.js'
15
14
  export * from './utils/utils.js'
16
15
 
17
16
  export async function runPlugin(plugin: Plugin) {
@@ -1,52 +0,0 @@
1
- import { EventEmitter } from 'node:events';
2
- import { ChildProcess } from 'node:child_process';
3
- import { Readable } from 'stream';
4
- import { mock } from 'node:test';
5
- import { AssertionError } from 'chai';
6
- import { CodifyTestUtils } from './test-utils.js';
7
- import { describe, expect, it } from 'vitest';
8
-
9
- describe('Test Utils tests', async () => {
10
-
11
- const mockChildProcess = () => {
12
- const process = new ChildProcess();
13
- process.stdout = new EventEmitter() as Readable;
14
- process.stderr = new EventEmitter() as Readable
15
- process.send = () => true;
16
-
17
- return process;
18
- }
19
-
20
- it('send a message', async () => {
21
- const process = mockChildProcess();
22
- const sendMock = mock.method(process, 'send');
23
-
24
- CodifyTestUtils.sendMessageToProcessAwaitResponse(process, { cmd: 'message', data: 'data' })
25
-
26
- expect(sendMock.mock.calls.length).to.eq(1);
27
- expect(sendMock.mock.calls[0].arguments[0]).to.deep.eq({ cmd: 'message', data: 'data' });
28
- })
29
-
30
- it('send a message and receives the response', async () => {
31
- const process = mockChildProcess();
32
-
33
- try {
34
- const result = await Promise.all([
35
- (async () => {
36
- await sleep(30);
37
- process.emit('message', { cmd: 'messageResult', data: 'data' })
38
- })(),
39
- CodifyTestUtils.sendMessageToProcessAwaitResponse(process, { cmd: 'message', data: 'data' }),
40
- ]);
41
-
42
- expect(result[1]).to.deep.eq({ cmd: 'messageResult', data: 'data' })
43
- } catch (e) {
44
- console.log(e);
45
- throw new AssertionError('Failed to receive message');
46
- }
47
- });
48
- });
49
-
50
- async function sleep(ms: number) {
51
- return new Promise((resolve, reject) => setTimeout(resolve, ms))
52
- }
@@ -1,20 +0,0 @@
1
- import { ChildProcess } from 'child_process';
2
-
3
- export class CodifyTestUtils {
4
- static sendMessageToProcessAwaitResponse(process: ChildProcess, message: any): Promise<any> {
5
- return new Promise((resolve, reject) => {
6
- process.on('message', (response) => {
7
- resolve(response)
8
- });
9
- process.on('error', (err) => reject(err))
10
- process.on('exit', (code) => {
11
- if (code != 0) {
12
- reject('Exit code is not 0');
13
- }
14
- resolve(code);
15
- })
16
- process.send(message);
17
- });
18
- }
19
-
20
- }