codify-plugin-lib 1.0.54 → 1.0.56

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.
@@ -20,4 +20,5 @@ export declare class ChangeSet<T extends StringIndexedObject> {
20
20
  static isSame(desired: unknown, current: unknown, options?: ParameterOptions): boolean;
21
21
  private static calculateStatefulModeChangeSet;
22
22
  private static calculateStatelessModeChangeSet;
23
+ private static addDefaultValues;
23
24
  }
@@ -61,6 +61,7 @@ export class ChangeSet {
61
61
  const parameterChangeSet = new Array();
62
62
  const _desired = { ...desired };
63
63
  const _current = { ...current };
64
+ this.addDefaultValues(_desired, parameterOptions);
64
65
  for (const [k, v] of Object.entries(_current)) {
65
66
  if (_desired[k] == null) {
66
67
  parameterChangeSet.push({
@@ -109,6 +110,7 @@ export class ChangeSet {
109
110
  const parameterChangeSet = new Array();
110
111
  const _desired = { ...desired };
111
112
  const _current = { ...current };
113
+ this.addDefaultValues(_desired, parameterOptions);
112
114
  for (const [k, v] of Object.entries(_desired)) {
113
115
  if (_current[k] == null) {
114
116
  parameterChangeSet.push({
@@ -137,4 +139,14 @@ export class ChangeSet {
137
139
  }
138
140
  return parameterChangeSet;
139
141
  }
142
+ static addDefaultValues(obj, options) {
143
+ Object.entries(options ?? {})
144
+ .filter(([, option]) => option.default !== undefined)
145
+ .map(([name, option]) => [name, option.default])
146
+ .forEach(([key, defaultValue]) => {
147
+ if (obj[key] === undefined) {
148
+ obj[key] = defaultValue;
149
+ }
150
+ });
151
+ }
140
152
  }
@@ -3,6 +3,7 @@ export interface ParameterOptions {
3
3
  planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
4
4
  isEqual?: (desired: any, current: any) => boolean;
5
5
  isElementEqual?: (desired: any, current: any) => boolean;
6
+ default?: unknown;
6
7
  isStatefulParameter?: boolean;
7
8
  }
8
9
  export interface PlanOptions<T> {
@@ -2,9 +2,10 @@ import { Resource } from './resource.js';
2
2
  import { ApplyRequestData, InitializeResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
3
3
  import { Plan } from './plan.js';
4
4
  export declare class Plugin {
5
+ name: string;
5
6
  resources: Map<string, Resource<ResourceConfig>>;
6
7
  planStorage: Map<string, Plan<ResourceConfig>>;
7
- constructor(resources: Map<string, Resource<ResourceConfig>>);
8
+ constructor(name: string, resources: Map<string, Resource<ResourceConfig>>);
8
9
  initialize(): Promise<InitializeResponseData>;
9
10
  validate(data: ValidateRequestData): Promise<ValidateResponseData>;
10
11
  plan(data: PlanRequestData): Promise<PlanResponseData>;
@@ -1,9 +1,11 @@
1
1
  import { Plan } from './plan.js';
2
2
  import { splitUserConfig } from '../utils/utils.js';
3
3
  export class Plugin {
4
+ name;
4
5
  resources;
5
6
  planStorage;
6
- constructor(resources) {
7
+ constructor(name, resources) {
8
+ this.name = name;
7
9
  this.resources = resources;
8
10
  this.planStorage = new Map();
9
11
  }
@@ -29,7 +29,6 @@ export declare abstract class Resource<T extends StringIndexedObject> {
29
29
  private _applyModify;
30
30
  private _applyDestroy;
31
31
  private validateRefreshResults;
32
- private applyDefaultValueToRefreshResults;
33
32
  private applyTransformParameters;
34
33
  private addDefaultValues;
35
34
  private refreshResourceParameters;
@@ -60,7 +60,7 @@ export class Resource {
60
60
  await this.applyTransformParameters(transformParameters, resourceParameters);
61
61
  const currentParameters = await this.refreshResourceParameters(resourceParameters);
62
62
  if (currentParameters == null) {
63
- return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
63
+ return Plan.create(resourceParameters, null, resourceMetadata, planOptions);
64
64
  }
65
65
  const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
66
66
  return Plan.create({ ...resourceParameters, ...statefulParameters }, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
@@ -142,10 +142,8 @@ export class Resource {
142
142
  if (!refresh) {
143
143
  return;
144
144
  }
145
- const desiredKeys = new Set([...desiredMap.keys()]
146
- .filter((key) => this.defaultValues[key] === undefined));
147
- const refreshKeys = new Set([...Object.keys(refresh)]
148
- .filter((key) => this.defaultValues[key] === undefined));
145
+ const desiredKeys = new Set(desiredMap.keys());
146
+ const refreshKeys = new Set(Object.keys(refresh));
149
147
  if (!setsEqual(desiredKeys, refreshKeys)) {
150
148
  throw new Error(`Resource ${this.typeId}
151
149
  refresh() must return back exactly the keys that were provided
@@ -153,30 +151,17 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
153
151
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
154
152
  }
155
153
  }
156
- applyDefaultValueToRefreshResults(refresh) {
157
- if (!refresh) {
158
- return;
159
- }
160
- for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
161
- if (refresh[key] === undefined) {
162
- refresh[key] = defaultValue;
163
- }
164
- }
165
- }
166
154
  async applyTransformParameters(transformParameters, desired) {
167
155
  const orderedEntries = [...Object.entries(transformParameters)]
168
156
  .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
169
- for (const [key] of orderedEntries) {
170
- if (desired[key] !== null) {
171
- const transformedValue = await this.transformParameters.get(key).transform(desired[key]);
172
- if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
173
- throw new Error(`Transform parameter ${key} is attempting to override existing value ${desired[key]}`);
174
- }
175
- Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
176
- desired[tvKey] = tvValue;
177
- });
178
- delete desired[key];
157
+ for (const [key, value] of orderedEntries) {
158
+ const transformedValue = await this.transformParameters.get(key).transform(value);
159
+ if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
160
+ throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
179
161
  }
162
+ Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
163
+ desired[tvKey] = tvValue;
164
+ });
180
165
  }
181
166
  }
182
167
  addDefaultValues(desired) {
@@ -191,7 +176,6 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
191
176
  const entriesToRefresh = new Map(Object.entries(resourceParameters));
192
177
  const currentParameters = await this.refresh(entriesToRefresh);
193
178
  this.validateRefreshResults(currentParameters, entriesToRefresh);
194
- this.applyDefaultValueToRefreshResults(currentParameters);
195
179
  return currentParameters;
196
180
  }
197
181
  async refreshStatefulParameters(statefulParametersConfig, isStatefulMode) {
package/dist/index.d.ts CHANGED
@@ -9,4 +9,3 @@ export * from './entities/stateful-parameter.js';
9
9
  export * from './utils/test-utils.js';
10
10
  export * from './utils/utils.js';
11
11
  export declare function runPlugin(plugin: Plugin): Promise<void>;
12
- export { ErrorMessage } from './entities/resource-types.js';
@@ -8,4 +8,5 @@ export declare class MessageHandler {
8
8
  constructor(plugin: Plugin);
9
9
  onMessage(message: unknown): Promise<void>;
10
10
  private validateMessage;
11
+ private handleErrors;
11
12
  }
@@ -44,39 +44,48 @@ export class MessageHandler {
44
44
  .map(([k, v]) => [k, this.ajv.compile(v.responseValidator)]));
45
45
  }
46
46
  async onMessage(message) {
47
- if (!this.validateMessage(message)) {
48
- throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`);
49
- }
50
- if (!this.requestValidators.has(message.cmd)) {
51
- throw new Error(`Plugin: ${this.plugin}. Unsupported message: ${message.cmd}`);
52
- }
53
- const requestValidator = this.requestValidators.get(message.cmd);
54
- if (!requestValidator(message.data)) {
55
- throw new Error(`Plugin: ${this.plugin}. cmd: ${message.cmd}. Malformed message data: ${JSON.stringify(requestValidator.errors, null, 2)}`);
56
- }
57
- let result;
58
47
  try {
59
- result = await SupportedRequests[message.cmd].handler(this.plugin, message.data);
60
- }
61
- catch (e) {
48
+ if (!this.validateMessage(message)) {
49
+ throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`);
50
+ }
51
+ if (!this.requestValidators.has(message.cmd)) {
52
+ throw new Error(`Plugin: ${this.plugin}. Unsupported message: ${message.cmd}`);
53
+ }
54
+ const requestValidator = this.requestValidators.get(message.cmd);
55
+ if (!requestValidator(message.data)) {
56
+ throw new Error(`Plugin: ${this.plugin}. cmd: ${message.cmd}. Malformed message data: ${JSON.stringify(requestValidator.errors, null, 2)}`);
57
+ }
58
+ const result = await SupportedRequests[message.cmd].handler(this.plugin, message.data);
59
+ const responseValidator = this.responseValidators.get(message.cmd);
60
+ if (responseValidator && !responseValidator(result)) {
61
+ throw new Error(`Plugin: ${this.plugin}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}`);
62
+ }
62
63
  process.send({
63
64
  cmd: message.cmd + '_Response',
64
- status: MessageStatus.ERROR,
65
- data: e.message,
65
+ status: MessageStatus.SUCCESS,
66
+ data: result,
66
67
  });
67
- return;
68
68
  }
69
- const responseValidator = this.responseValidators.get(message.cmd);
70
- if (responseValidator && !responseValidator(result)) {
71
- throw new Error(`Plugin: ${this.plugin}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}`);
69
+ catch (e) {
70
+ this.handleErrors(message, e);
72
71
  }
73
- process.send({
74
- cmd: message.cmd + '_Response',
75
- status: MessageStatus.SUCCESS,
76
- data: result,
77
- });
78
72
  }
79
73
  validateMessage(message) {
80
74
  return this.messageSchemaValidator(message);
81
75
  }
76
+ handleErrors(message, e) {
77
+ if (!message) {
78
+ return;
79
+ }
80
+ if (!message.hasOwnProperty('cmd')) {
81
+ return;
82
+ }
83
+ const cmd = message.cmd + '_Response';
84
+ const isDebug = process.env.DEBUG?.includes('*') ?? false;
85
+ process.send?.({
86
+ cmd,
87
+ status: MessageStatus.ERROR,
88
+ data: isDebug ? e.stack : e.message,
89
+ });
90
+ }
82
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.54",
3
+ "version": "1.0.56",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -126,6 +126,8 @@ export class ChangeSet<T extends StringIndexedObject> {
126
126
  const _desired = { ...desired };
127
127
  const _current = { ...current };
128
128
 
129
+ this.addDefaultValues(_desired, parameterOptions);
130
+
129
131
  for (const [k, v] of Object.entries(_current)) {
130
132
  if (_desired[k] == null) {
131
133
  parameterChangeSet.push({
@@ -191,6 +193,8 @@ export class ChangeSet<T extends StringIndexedObject> {
191
193
  const _desired = { ...desired };
192
194
  const _current = { ...current };
193
195
 
196
+ this.addDefaultValues(_desired, parameterOptions);
197
+
194
198
  for (const [k, v] of Object.entries(_desired)) {
195
199
  if (_current[k] == null) {
196
200
  parameterChangeSet.push({
@@ -224,5 +228,16 @@ export class ChangeSet<T extends StringIndexedObject> {
224
228
 
225
229
  return parameterChangeSet;
226
230
  }
231
+
232
+ private static addDefaultValues<T extends StringIndexedObject>(obj: Record<string, unknown>, options?: Record<keyof T, ParameterOptions>) {
233
+ Object.entries(options ?? {})
234
+ .filter(([, option]) => option.default !== undefined)
235
+ .map(([name, option]) => [name, option.default] as const)
236
+ .forEach(([key, defaultValue]) => {
237
+ if (obj[key] === undefined) {
238
+ obj[key] = defaultValue;
239
+ }
240
+ })
241
+ }
227
242
 
228
243
  }
@@ -17,6 +17,8 @@ export interface ParameterOptions {
17
17
 
18
18
  isElementEqual?: (desired: any, current: any) => boolean;
19
19
 
20
+ default?: unknown,
21
+
20
22
  isStatefulParameter?: boolean;
21
23
  }
22
24
 
@@ -15,6 +15,7 @@ export class Plugin {
15
15
  planStorage: Map<string, Plan<ResourceConfig>>;
16
16
 
17
17
  constructor(
18
+ public name: string,
18
19
  public resources: Map<string, Resource<ResourceConfig>>
19
20
  ) {
20
21
  this.planStorage = new Map();
@@ -27,7 +27,7 @@ class TestParameter extends StatefulParameter<TestConfig, string> {
27
27
  }
28
28
  }
29
29
 
30
- describe('Resource parameters tests', () => {
30
+ describe('Resource parameter tests', () => {
31
31
  it('supports the creation of stateful parameters', async () => {
32
32
 
33
33
  const statefulParameter = new class extends TestParameter {
@@ -26,7 +26,7 @@ export class TestResource extends Resource<TestConfig> {
26
26
  return Promise.resolve(undefined);
27
27
  }
28
28
 
29
- async refresh(): Promise<Partial<TestConfig> | null> {
29
+ async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
30
30
  return {
31
31
  propA: 'a',
32
32
  propB: 10,
@@ -329,8 +329,12 @@ describe('Resource tests', () => {
329
329
  });
330
330
  }
331
331
 
332
- async refresh(): Promise<Partial<TestConfig> | null> {
333
- return {};
332
+ async refresh(keys: Map<string, unknown>): Promise<Partial<TestConfig> | null> {
333
+ expect(keys.has('propE')).to.be.true;
334
+
335
+ return {
336
+ propE: keys.get('propE'),
337
+ };
334
338
  }
335
339
  }
336
340
 
@@ -340,6 +344,28 @@ describe('Resource tests', () => {
340
344
  expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
341
345
  })
342
346
 
347
+ it('Allows default values to be added even when refresh returns null', async () => {
348
+ const resource = new class extends TestResource {
349
+ constructor() {
350
+ super({
351
+ type: 'type',
352
+ parameterOptions: {
353
+ propE: { default: 'propEDefault' }
354
+ }
355
+ });
356
+ }
357
+
358
+ async refresh(): Promise<Partial<TestConfig> | null> {
359
+ return null;
360
+ }
361
+ }
362
+
363
+ const plan = await resource.plan({ type: 'resource'})
364
+ expect(plan.currentConfig.propE).to.eq(null);
365
+ expect(plan.desiredConfig.propE).to.eq('propEDefault');
366
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
367
+ })
368
+
343
369
  it('Allows default values to be added (ignore default value if already present)', async () => {
344
370
  const resource = new class extends TestResource {
345
371
  constructor() {
@@ -101,7 +101,7 @@ export abstract class Resource<T extends StringIndexedObject> {
101
101
 
102
102
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
103
103
  if (currentParameters == null) {
104
- return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
104
+ return Plan.create(resourceParameters, null, resourceMetadata, planOptions);
105
105
  }
106
106
 
107
107
  // Refresh stateful parameters. These parameters have state external to the resource
@@ -211,15 +211,8 @@ export abstract class Resource<T extends StringIndexedObject> {
211
211
  return;
212
212
  }
213
213
 
214
- const desiredKeys = new Set<keyof T>(
215
- [...desiredMap.keys()]
216
- .filter((key) => this.defaultValues[key] === undefined)
217
- );
218
-
219
- const refreshKeys = new Set(
220
- [...Object.keys(refresh)]
221
- .filter((key) => this.defaultValues[key] === undefined)
222
- ) as Set<keyof T>;
214
+ const desiredKeys = new Set<keyof T>(desiredMap.keys());
215
+ const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
223
216
 
224
217
  if (!setsEqual(desiredKeys, refreshKeys)) {
225
218
  throw new Error(
@@ -231,38 +224,21 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
231
224
  }
232
225
  }
233
226
 
234
- private applyDefaultValueToRefreshResults(refresh: Partial<T> | null) {
235
- if (!refresh) {
236
- return;
237
- }
238
-
239
- for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
240
- if (refresh[key] === undefined) {
241
- // @ts-ignore
242
- refresh[key] = defaultValue;
243
- }
244
- }
245
- }
246
-
247
227
  private async applyTransformParameters(transformParameters: Partial<T>, desired: Partial<T>): Promise<void> {
248
228
  const orderedEntries = [...Object.entries(transformParameters)]
249
229
  .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
250
230
 
251
- for (const [key] of orderedEntries) {
252
- if (desired[key] !== null) {
253
- const transformedValue = await this.transformParameters.get(key)!.transform(desired[key]);
254
-
255
- if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
256
- throw new Error(`Transform parameter ${key as string} is attempting to override existing value ${desired[key]}`);
257
- }
258
-
259
- Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
260
- // @ts-ignore
261
- desired[tvKey] = tvValue;
262
- })
231
+ for (const [key, value] of orderedEntries) {
232
+ const transformedValue = await this.transformParameters.get(key)!.transform(value);
263
233
 
264
- delete desired[key];
234
+ if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
235
+ throw new Error(`Transform parameter ${key as string} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
265
236
  }
237
+
238
+ Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
239
+ // @ts-ignore
240
+ desired[tvKey] = tvValue;
241
+ })
266
242
  }
267
243
  }
268
244
 
@@ -281,8 +257,6 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
281
257
  const currentParameters = await this.refresh(entriesToRefresh);
282
258
 
283
259
  this.validateRefreshResults(currentParameters, entriesToRefresh);
284
- this.applyDefaultValueToRefreshResults(currentParameters);
285
-
286
260
  return currentParameters;
287
261
  }
288
262
 
@@ -1,5 +1,11 @@
1
1
  import { StringIndexedObject } from 'codify-schemas';
2
2
 
3
+ /**
4
+ * Transform parameters convert the provided value into
5
+ * other parameters. Transform parameters will not show up
6
+ * in the refresh or the plan. Transform parameters get processed after
7
+ * default values.
8
+ */
3
9
  export abstract class TransformParameter<T extends StringIndexedObject> {
4
10
 
5
11
  abstract transform(value: any): Promise<Partial<T>>
package/src/index.ts CHANGED
@@ -16,4 +16,3 @@ export async function runPlugin(plugin: Plugin) {
16
16
  const messageHandler = new MessageHandler(plugin);
17
17
  process.on('message', (message) => messageHandler.onMessage(message))
18
18
  }
19
- export { ErrorMessage } from './entities/resource-types.js';
@@ -1,14 +1,25 @@
1
1
  import { MessageHandler } from './handlers.js';
2
2
  import { Plugin } from '../entities/plugin.js';
3
- import { describe, it, expect } from 'vitest';
4
- import { vi } from 'vitest'
3
+ import { describe, expect, it } from 'vitest';
5
4
  import { mock } from 'vitest-mock-extended'
5
+ import { Resource } from '../entities/resource.js';
6
+ import { Plan } from '../entities/plan.js';
7
+ import { MessageStatus, ResourceOperation } from 'codify-schemas';
6
8
 
7
9
  describe('Message handler tests', () => {
8
10
  it('handles plan requests', async () => {
9
11
  const plugin = mock<Plugin>();
10
12
  const handler = new MessageHandler(plugin);
11
13
 
14
+ process.send = (message) => {
15
+ expect(message).toMatchObject({
16
+ cmd: 'plan_Response',
17
+ status: MessageStatus.SUCCESS,
18
+ });
19
+
20
+ return true;
21
+ }
22
+
12
23
  // Message handler also validates the response. That part does not need to be tested
13
24
  try {
14
25
  await handler.onMessage({
@@ -23,12 +34,23 @@ describe('Message handler tests', () => {
23
34
  } catch (e) {}
24
35
 
25
36
  expect(plugin.plan.mock.calls.length).to.eq(1);
37
+ process.send = undefined;
26
38
  })
27
39
 
28
40
  it('rejects bad plan requests', async () => {
29
41
  const plugin = mock<Plugin>();
30
42
  const handler = new MessageHandler(plugin);
31
43
 
44
+ process.send = (message) => {
45
+ console.log(message);
46
+ expect(message).toMatchObject({
47
+ cmd: 'plan_Response',
48
+ status: MessageStatus.ERROR,
49
+ });
50
+
51
+ return true;
52
+ }
53
+
32
54
  // Message handler also validates the response. That part does not need to be tested
33
55
  try {
34
56
  await handler.onMessage({
@@ -39,9 +61,12 @@ describe('Message handler tests', () => {
39
61
  prop2: 'B',
40
62
  }
41
63
  })
42
- } catch (e) {}
64
+ } catch (e) {
65
+ console.log(e);
66
+ }
43
67
 
44
68
  expect(plugin.plan.mock.calls.length).to.eq(0);
69
+ process.send = undefined;
45
70
  })
46
71
 
47
72
  it('handles apply requests', async () => {
@@ -110,15 +135,123 @@ describe('Message handler tests', () => {
110
135
  const plugin = mock<Plugin>();
111
136
  const handler = new MessageHandler(plugin);
112
137
 
138
+ process.send = () => true;
139
+
113
140
  // Message handler also validates the response. That part does not need to be tested
114
- try {
115
- await handler.onMessage({
116
- cmd: 'validate',
117
- data: {}
118
- })
119
- } catch (e) {}
141
+ // This should not throw
142
+ expect(await handler.onMessage({
143
+ cmd: 'validate',
144
+ data: {}
145
+ })).to.eq(undefined);
120
146
 
121
147
  expect(plugin.apply.mock.calls.length).to.be.eq(0);
122
148
  })
123
149
 
150
+ it('handles errors for plan', async () => {
151
+ const resource= testResource();
152
+ const plugin = testPlugin(resource);
153
+
154
+ const handler = new MessageHandler(plugin);
155
+
156
+ process.send = (message) => {
157
+ expect(message).toMatchObject({
158
+ cmd: 'plan_Response',
159
+ status: MessageStatus.ERROR,
160
+ data: 'Refresh error',
161
+ })
162
+ return true;
163
+ }
164
+
165
+ expect(async () => await handler.onMessage({
166
+ cmd: 'plan',
167
+ data: {
168
+ type: 'resourceA'
169
+ }
170
+ })).rejects.to.not.throw;
171
+
172
+ process.send = undefined;
173
+ })
174
+
175
+ it('handles errors for apply (create)', async () => {
176
+ const resource= testResource();
177
+ const plugin = testPlugin(resource);
178
+
179
+ const handler = new MessageHandler(plugin);
180
+
181
+ process.send = (message) => {
182
+ expect(message).toMatchObject({
183
+ cmd: 'apply_Response',
184
+ status: MessageStatus.ERROR,
185
+ data: 'Create error',
186
+ })
187
+ return true;
188
+ }
189
+
190
+ expect(async () => await handler.onMessage({
191
+ cmd: 'apply',
192
+ data: {
193
+ plan: {
194
+ resourceType: 'resourceA',
195
+ operation: ResourceOperation.CREATE,
196
+ parameters: []
197
+ }
198
+ }
199
+ })).rejects.to.not.throw;
200
+ })
201
+
202
+ it('handles errors for apply (destroy)', async () => {
203
+ const resource= testResource();
204
+ const plugin = testPlugin(resource);
205
+
206
+ const handler = new MessageHandler(plugin);
207
+
208
+ process.send = (message) => {
209
+ expect(message).toMatchObject({
210
+ cmd: 'apply_Response',
211
+ status: MessageStatus.ERROR,
212
+ data: 'Destroy error',
213
+ })
214
+ return true;
215
+ }
216
+
217
+ expect(async () => await handler.onMessage({
218
+ cmd: 'apply',
219
+ data: {
220
+ plan: {
221
+ resourceType: 'resourceA',
222
+ operation: ResourceOperation.DESTROY,
223
+ parameters: []
224
+ }
225
+ }
226
+ })).rejects.to.not.throw;
227
+ })
228
+
229
+
230
+ const testResource = () => new class extends Resource<any> {
231
+ constructor() {
232
+ super({ type: 'resourceA' });
233
+ }
234
+
235
+ async refresh(keys: Map<keyof any, any>): Promise<Partial<any> | null> {
236
+ throw new Error('Refresh error');
237
+ }
238
+
239
+ applyCreate(plan: Plan<any>): Promise<void> {
240
+ throw new Error('Create error');
241
+ }
242
+
243
+ applyDestroy(plan: Plan<any>): Promise<void> {
244
+ throw new Error('Destroy error');
245
+ }
246
+ }
247
+
248
+ const testPlugin = (resource: Resource<any>) => new class extends Plugin {
249
+ constructor() {
250
+ const map = new Map();
251
+ map.set('resourceA', resource);
252
+
253
+ super('name', map);
254
+ }
255
+ }
256
+
124
257
  });
@@ -67,45 +67,60 @@ export class MessageHandler {
67
67
  }
68
68
 
69
69
  async onMessage(message: unknown): Promise<void> {
70
- if (!this.validateMessage(message)) {
71
- throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`);
72
- }
70
+ try {
71
+ if (!this.validateMessage(message)) {
72
+ throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`);
73
+ }
73
74
 
74
- if (!this.requestValidators.has(message.cmd)) {
75
- throw new Error(`Plugin: ${this.plugin}. Unsupported message: ${message.cmd}`);
76
- }
75
+ if (!this.requestValidators.has(message.cmd)) {
76
+ throw new Error(`Plugin: ${this.plugin}. Unsupported message: ${message.cmd}`);
77
+ }
77
78
 
78
- const requestValidator = this.requestValidators.get(message.cmd)!;
79
- if (!requestValidator(message.data)) {
80
- throw new Error(`Plugin: ${this.plugin}. cmd: ${message.cmd}. Malformed message data: ${JSON.stringify(requestValidator.errors, null, 2)}`)
81
- }
79
+ const requestValidator = this.requestValidators.get(message.cmd)!;
80
+ if (!requestValidator(message.data)) {
81
+ throw new Error(`Plugin: ${this.plugin}. cmd: ${message.cmd}. Malformed message data: ${JSON.stringify(requestValidator.errors, null, 2)}`)
82
+ }
83
+
84
+ const result = await SupportedRequests[message.cmd].handler(this.plugin, message.data);
85
+
86
+ const responseValidator = this.responseValidators.get(message.cmd);
87
+ if (responseValidator && !responseValidator(result)) {
88
+ throw new Error(`Plugin: ${this.plugin}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}`)
89
+ }
82
90
 
83
- let result: unknown;
84
- try {
85
- result = await SupportedRequests[message.cmd].handler(this.plugin, message.data);
86
- } catch(e: any) {
87
91
  process.send!({
88
92
  cmd: message.cmd + '_Response',
89
- status: MessageStatus.ERROR,
90
- data: e.message,
93
+ status: MessageStatus.SUCCESS,
94
+ data: result,
91
95
  })
92
96
 
97
+ } catch (e: unknown) {
98
+ this.handleErrors(message, e as Error);
99
+ }
100
+ }
101
+
102
+ private validateMessage(message: unknown): message is IpcMessage {
103
+ return this.messageSchemaValidator(message);
104
+ }
105
+
106
+ private handleErrors(message: unknown, e: Error) {
107
+ if (!message) {
93
108
  return;
94
109
  }
95
110
 
96
- const responseValidator = this.responseValidators.get(message.cmd);
97
- if (responseValidator && !responseValidator(result)) {
98
- throw new Error(`Plugin: ${this.plugin}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}`)
111
+ if (!message.hasOwnProperty('cmd')) {
112
+ return;
99
113
  }
100
114
 
101
- process.send!({
102
- cmd: message.cmd + '_Response',
103
- status: MessageStatus.SUCCESS,
104
- data: result,
105
- })
106
- }
115
+ // @ts-ignore
116
+ const cmd = message.cmd + '_Response';
107
117
 
108
- private validateMessage(message: unknown): message is IpcMessage {
109
- return this.messageSchemaValidator(message);
118
+ const isDebug = process.env.DEBUG?.includes('*') ?? false;
119
+
120
+ process.send?.({
121
+ cmd,
122
+ status: MessageStatus.ERROR,
123
+ data: isDebug ? e.stack : e.message,
124
+ })
110
125
  }
111
126
  }
@@ -3,8 +3,8 @@ import { ChildProcess } from 'node:child_process';
3
3
  import { Readable } from 'stream';
4
4
  import { mock } from 'node:test';
5
5
  import { AssertionError } from 'chai';
6
- import { CodifyTestUtils } from './test-utils';
7
- import { describe, it, expect } from 'vitest';
6
+ import { CodifyTestUtils } from './test-utils.js';
7
+ import { describe, expect, it } from 'vitest';
8
8
 
9
9
  describe('Test Utils tests', async () => {
10
10