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.
- package/dist/entities/change-set.d.ts +1 -0
- package/dist/entities/change-set.js +12 -0
- package/dist/entities/plan-types.d.ts +1 -0
- package/dist/entities/plugin.d.ts +2 -1
- package/dist/entities/plugin.js +3 -1
- package/dist/entities/resource.d.ts +0 -1
- package/dist/entities/resource.js +10 -26
- package/dist/index.d.ts +0 -1
- package/dist/messages/handlers.d.ts +1 -0
- package/dist/messages/handlers.js +34 -25
- package/package.json +1 -1
- package/src/entities/change-set.ts +15 -0
- package/src/entities/plan-types.ts +2 -0
- package/src/entities/plugin.ts +1 -0
- package/src/entities/resource-parameters.test.ts +1 -1
- package/src/entities/resource.test.ts +29 -3
- package/src/entities/resource.ts +12 -38
- package/src/entities/transform-parameter.ts +6 -0
- package/src/index.ts +0 -1
- package/src/messages/handlers.test.ts +142 -9
- package/src/messages/handlers.ts +42 -27
- package/src/utils/test-utils.test.ts +2 -2
|
@@ -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>;
|
package/dist/entities/plugin.js
CHANGED
|
@@ -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(
|
|
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(
|
|
146
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
65
|
-
data:
|
|
65
|
+
status: MessageStatus.SUCCESS,
|
|
66
|
+
data: result,
|
|
66
67
|
});
|
|
67
|
-
return;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
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
|
@@ -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
|
}
|
package/src/entities/plugin.ts
CHANGED
|
@@ -27,7 +27,7 @@ class TestParameter extends StatefulParameter<TestConfig, string> {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
describe('Resource
|
|
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
|
-
|
|
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() {
|
package/src/entities/resource.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { MessageHandler } from './handlers.js';
|
|
2
2
|
import { Plugin } from '../entities/plugin.js';
|
|
3
|
-
import { describe,
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
});
|
package/src/messages/handlers.ts
CHANGED
|
@@ -67,45 +67,60 @@ export class MessageHandler {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async onMessage(message: unknown): Promise<void> {
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
if (!this.requestValidators.has(message.cmd)) {
|
|
76
|
+
throw new Error(`Plugin: ${this.plugin}. Unsupported message: ${message.cmd}`);
|
|
77
|
+
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
90
|
-
data:
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
status: MessageStatus.SUCCESS,
|
|
104
|
-
data: result,
|
|
105
|
-
})
|
|
106
|
-
}
|
|
115
|
+
// @ts-ignore
|
|
116
|
+
const cmd = message.cmd + '_Response';
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
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
|
|