codify-plugin-lib 1.0.55 → 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.
@@ -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
  }
@@ -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);
@@ -154,17 +154,14 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
154
154
  async applyTransformParameters(transformParameters, desired) {
155
155
  const orderedEntries = [...Object.entries(transformParameters)]
156
156
  .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
157
- for (const [key] of orderedEntries) {
158
- if (desired[key] !== null) {
159
- const transformedValue = await this.transformParameters.get(key).transform(desired[key]);
160
- if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
161
- throw new Error(`Transform parameter ${key} is attempting to override existing value ${desired[key]}`);
162
- }
163
- Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
164
- desired[tvKey] = tvValue;
165
- });
166
- 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)}`);
167
161
  }
162
+ Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
163
+ desired[tvKey] = tvValue;
164
+ });
168
165
  }
169
166
  }
170
167
  addDefaultValues(desired) {
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.55",
3
+ "version": "1.0.56",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -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();
@@ -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
@@ -228,21 +228,17 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
228
228
  const orderedEntries = [...Object.entries(transformParameters)]
229
229
  .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA)! - this.transformParameterOrder.get(keyB)!)
230
230
 
231
- for (const [key] of orderedEntries) {
232
- if (desired[key] !== null) {
233
- const transformedValue = await this.transformParameters.get(key)!.transform(desired[key]);
231
+ for (const [key, value] of orderedEntries) {
232
+ const transformedValue = await this.transformParameters.get(key)!.transform(value);
234
233
 
235
- if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
236
- throw new Error(`Transform parameter ${key as string} is attempting to override existing value ${desired[key]}`);
237
- }
238
-
239
- Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
240
- // @ts-ignore
241
- desired[tvKey] = tvValue;
242
- })
243
-
244
- 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)}`);
245
236
  }
237
+
238
+ Object.entries(transformedValue).forEach(([tvKey, tvValue]) => {
239
+ // @ts-ignore
240
+ desired[tvKey] = tvValue;
241
+ })
246
242
  }
247
243
  }
248
244
 
@@ -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