codify-plugin-lib 1.0.144 → 1.0.145
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/messages/handlers.js +6 -1
- package/dist/plan/plan.js +8 -24
- package/dist/plugin/plugin.d.ts +2 -1
- package/dist/plugin/plugin.js +22 -3
- package/dist/resource/parsed-resource-settings.d.ts +2 -1
- package/dist/resource/parsed-resource-settings.js +4 -1
- package/dist/resource/resource-settings.d.ts +2 -1
- package/dist/resource/resource-settings.js +31 -0
- package/package.json +2 -2
- package/src/messages/handlers.ts +7 -0
- package/src/plan/plan.ts +9 -30
- package/src/plugin/plugin.test.ts +32 -8
- package/src/plugin/plugin.ts +31 -4
- package/src/resource/parsed-resource-settings.ts +6 -1
- package/src/resource/resource-settings.test.ts +65 -0
- package/src/resource/resource-settings.ts +42 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ajv } from 'ajv';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
|
-
import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
|
|
3
|
+
import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MatchRequestDataSchema, MatchResponseDataSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
|
|
4
4
|
import { SudoError } from '../errors.js';
|
|
5
5
|
const SupportedRequests = {
|
|
6
6
|
'initialize': {
|
|
@@ -18,6 +18,11 @@ const SupportedRequests = {
|
|
|
18
18
|
requestValidator: GetResourceInfoRequestDataSchema,
|
|
19
19
|
responseValidator: GetResourceInfoResponseDataSchema
|
|
20
20
|
},
|
|
21
|
+
'match': {
|
|
22
|
+
handler: async (plugin, data) => plugin.match(data),
|
|
23
|
+
requestValidator: MatchRequestDataSchema,
|
|
24
|
+
responseValidator: MatchResponseDataSchema
|
|
25
|
+
},
|
|
21
26
|
'import': {
|
|
22
27
|
handler: async (plugin, data) => plugin.import(data),
|
|
23
28
|
requestValidator: ImportRequestDataSchema,
|
package/dist/plan/plan.js
CHANGED
|
@@ -156,30 +156,14 @@ export class Plan {
|
|
|
156
156
|
if (!currentArray) {
|
|
157
157
|
return null;
|
|
158
158
|
}
|
|
159
|
-
const matcher
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!currentParameter) {
|
|
168
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
if (!desiredParameter) {
|
|
172
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
return currentParameter === desiredParameter;
|
|
176
|
-
}));
|
|
177
|
-
if (matched.length > 1) {
|
|
178
|
-
console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`);
|
|
179
|
-
}
|
|
180
|
-
return matched[0];
|
|
181
|
-
})
|
|
182
|
-
: settings.allowMultiple.matcher;
|
|
159
|
+
const { matcher: parameterMatcher, id } = settings;
|
|
160
|
+
const matcher = (desired, currentArray) => {
|
|
161
|
+
const matched = currentArray.filter((c) => parameterMatcher(desired, c));
|
|
162
|
+
if (matched.length > 0) {
|
|
163
|
+
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`);
|
|
164
|
+
}
|
|
165
|
+
return matched[0];
|
|
166
|
+
};
|
|
183
167
|
if (isStateful) {
|
|
184
168
|
return state
|
|
185
169
|
? matcher(state, currentArray) ?? null
|
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
1
|
+
import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeResponseData, MatchRequestData, MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
2
2
|
import { Plan } from '../plan/plan.js';
|
|
3
3
|
import { BackgroundPty } from '../pty/background-pty.js';
|
|
4
4
|
import { Resource } from '../resource/resource.js';
|
|
@@ -12,6 +12,7 @@ export declare class Plugin {
|
|
|
12
12
|
static create(name: string, resources: Resource<any>[]): Plugin;
|
|
13
13
|
initialize(): Promise<InitializeResponseData>;
|
|
14
14
|
getResourceInfo(data: GetResourceInfoRequestData): Promise<GetResourceInfoResponseData>;
|
|
15
|
+
match(data: MatchRequestData): Promise<MatchResponseData>;
|
|
15
16
|
import(data: ImportRequestData): Promise<ImportResponseData>;
|
|
16
17
|
validate(data: ValidateRequestData): Promise<ValidateResponseData>;
|
|
17
18
|
plan(data: PlanRequestData): Promise<PlanResponseData>;
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -42,9 +42,7 @@ export class Plugin {
|
|
|
42
42
|
?? schema?.required
|
|
43
43
|
?? undefined);
|
|
44
44
|
const allowMultiple = resource.settings.allowMultiple !== undefined
|
|
45
|
-
|
|
46
|
-
? { identifyingParameters: schema?.required ?? [] }
|
|
47
|
-
: { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] }) : undefined;
|
|
45
|
+
&& resource.settings.allowMultiple !== false;
|
|
48
46
|
return {
|
|
49
47
|
plugin: this.name,
|
|
50
48
|
type: data.type,
|
|
@@ -60,6 +58,27 @@ export class Plugin {
|
|
|
60
58
|
allowMultiple
|
|
61
59
|
};
|
|
62
60
|
}
|
|
61
|
+
async match(data) {
|
|
62
|
+
const { resource: resourceConfig, array } = data;
|
|
63
|
+
const resource = this.resourceControllers.get(resourceConfig.core.type);
|
|
64
|
+
if (!resource) {
|
|
65
|
+
throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`);
|
|
66
|
+
}
|
|
67
|
+
const parameterMatcher = resource?.parsedSettings.matcher;
|
|
68
|
+
const match = array.find((r) => {
|
|
69
|
+
if (resourceConfig.core.type !== r.core.type) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
|
|
73
|
+
if (resourceConfig.core.name === r.core.name
|
|
74
|
+
&& resourceConfig.core.name
|
|
75
|
+
&& Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return parameterMatcher(resourceConfig.parameters, r.parameters);
|
|
79
|
+
});
|
|
80
|
+
return { match };
|
|
81
|
+
}
|
|
63
82
|
async import(data) {
|
|
64
83
|
const { core, parameters } = data;
|
|
65
84
|
if (!this.resourceControllers.has(core.type)) {
|
|
@@ -20,7 +20,7 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
|
|
|
20
20
|
id: string;
|
|
21
21
|
schema?: Partial<JSONSchemaType<T | any>>;
|
|
22
22
|
allowMultiple?: {
|
|
23
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
23
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
24
24
|
requiredParameters?: string[];
|
|
25
25
|
} | boolean;
|
|
26
26
|
removeStatefulParametersBeforeDestroy?: boolean | undefined;
|
|
@@ -34,6 +34,7 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
|
|
|
34
34
|
get defaultValues(): Partial<Record<keyof T, unknown>>;
|
|
35
35
|
get inputTransformations(): Partial<Record<keyof T, InputTransformation>>;
|
|
36
36
|
get statefulParameterOrder(): Map<keyof T, number>;
|
|
37
|
+
get matcher(): (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
37
38
|
private validateSettings;
|
|
38
39
|
private validateParameterEqualsFn;
|
|
39
40
|
private getFromCacheOrCreate;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
|
|
2
|
-
import { resolveElementEqualsFn, resolveEqualsFn, resolveParameterTransformFn } from './resource-settings.js';
|
|
2
|
+
import { resolveElementEqualsFn, resolveEqualsFn, resolveMatcher, resolveParameterTransformFn } from './resource-settings.js';
|
|
3
3
|
export class ParsedResourceSettings {
|
|
4
4
|
cache = new Map();
|
|
5
5
|
id;
|
|
@@ -101,6 +101,9 @@ export class ParsedResourceSettings {
|
|
|
101
101
|
return new Map(resultArray.map((key, idx) => [key, idx]));
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
+
get matcher() {
|
|
105
|
+
return resolveMatcher(this);
|
|
106
|
+
}
|
|
104
107
|
validateSettings() {
|
|
105
108
|
// validate parameter settings
|
|
106
109
|
if (this.settings.parameterSettings) {
|
|
@@ -42,7 +42,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
42
42
|
*
|
|
43
43
|
* @return The matched resource.
|
|
44
44
|
*/
|
|
45
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
45
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
46
46
|
} | boolean;
|
|
47
47
|
/**
|
|
48
48
|
* If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
|
|
@@ -255,3 +255,4 @@ export declare function resolveEqualsFn(parameter: ParameterSetting): (desired:
|
|
|
255
255
|
export declare function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean;
|
|
256
256
|
export declare function resolveFnFromEqualsFnOrString(fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined): ((a: unknown, b: unknown) => boolean) | undefined;
|
|
257
257
|
export declare function resolveParameterTransformFn(parameter: ParameterSetting): InputTransformation | undefined;
|
|
258
|
+
export declare function resolveMatcher<T extends StringIndexedObject>(settings: ResourceSettings<T>): (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
@@ -85,3 +85,34 @@ export function resolveParameterTransformFn(parameter) {
|
|
|
85
85
|
}
|
|
86
86
|
return parameter.transformation ?? ParameterTransformationDefaults[parameter.type] ?? undefined;
|
|
87
87
|
}
|
|
88
|
+
export function resolveMatcher(settings) {
|
|
89
|
+
return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
|
|
90
|
+
? ((desired, current) => {
|
|
91
|
+
if (!desired || !current) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const requiredParameters = typeof settings.allowMultiple === 'object'
|
|
95
|
+
? settings.allowMultiple?.identifyingParameters ?? settings.schema?.required ?? []
|
|
96
|
+
: settings.schema?.required ?? [];
|
|
97
|
+
return requiredParameters.every((key) => {
|
|
98
|
+
const currentParameter = current[key];
|
|
99
|
+
const desiredParameter = desired[key];
|
|
100
|
+
// If both desired and current don't have a certain parameter then we assume they are the same
|
|
101
|
+
if (!currentParameter && !desiredParameter) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (!currentParameter) {
|
|
105
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!desiredParameter) {
|
|
109
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const parameterSetting = settings.parameterSettings?.[key];
|
|
113
|
+
const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null;
|
|
114
|
+
return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
|
|
115
|
+
});
|
|
116
|
+
})
|
|
117
|
+
: settings.allowMultiple.matcher;
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.145",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"ajv": "^8.12.0",
|
|
18
18
|
"ajv-formats": "^2.1.1",
|
|
19
|
-
"codify-schemas": "1.0.
|
|
19
|
+
"codify-schemas": "1.0.73",
|
|
20
20
|
"@npmcli/promise-spawn": "^7.0.1",
|
|
21
21
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
|
|
22
22
|
"uuid": "^10.0.0",
|
package/src/messages/handlers.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
IpcMessageSchema,
|
|
14
14
|
IpcMessageV2,
|
|
15
15
|
IpcMessageV2Schema,
|
|
16
|
+
MatchRequestDataSchema,
|
|
17
|
+
MatchResponseDataSchema,
|
|
16
18
|
MessageStatus,
|
|
17
19
|
PlanRequestDataSchema,
|
|
18
20
|
PlanResponseDataSchema,
|
|
@@ -40,6 +42,11 @@ const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) =
|
|
|
40
42
|
requestValidator: GetResourceInfoRequestDataSchema,
|
|
41
43
|
responseValidator: GetResourceInfoResponseDataSchema
|
|
42
44
|
},
|
|
45
|
+
'match': {
|
|
46
|
+
handler: async (plugin: Plugin, data: any) => plugin.match(data),
|
|
47
|
+
requestValidator: MatchRequestDataSchema,
|
|
48
|
+
responseValidator: MatchResponseDataSchema
|
|
49
|
+
},
|
|
43
50
|
'import': {
|
|
44
51
|
handler: async (plugin: Plugin, data: any) => plugin.import(data),
|
|
45
52
|
requestValidator: ImportRequestDataSchema,
|
package/src/plan/plan.ts
CHANGED
|
@@ -241,7 +241,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
241
241
|
desired: Partial<T> | null,
|
|
242
242
|
currentArray: Partial<T>[] | null,
|
|
243
243
|
state: Partial<T> | null,
|
|
244
|
-
settings:
|
|
244
|
+
settings: ParsedResourceSettings<T>,
|
|
245
245
|
isStateful: boolean,
|
|
246
246
|
}): Partial<T> | null {
|
|
247
247
|
const {
|
|
@@ -260,36 +260,15 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
const matcher
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const matched = currentArr.filter((c) => requiredParameters.every((key) => {
|
|
270
|
-
const currentParameter = c[key];
|
|
271
|
-
const desiredParameter = desired[key];
|
|
272
|
-
|
|
273
|
-
if (!currentParameter) {
|
|
274
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (!desiredParameter) {
|
|
279
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return currentParameter === desiredParameter;
|
|
284
|
-
}))
|
|
285
|
-
|
|
286
|
-
if (matched.length > 1) {
|
|
287
|
-
console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`);
|
|
288
|
-
}
|
|
263
|
+
const { matcher: parameterMatcher, id } = settings;
|
|
264
|
+
const matcher = (desired: Partial<T>, currentArray: Partial<T>[]): Partial<T> | undefined => {
|
|
265
|
+
const matched = currentArray.filter((c) => parameterMatcher(desired, c))
|
|
266
|
+
if (matched.length > 0) {
|
|
267
|
+
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`)
|
|
268
|
+
}
|
|
289
269
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
: settings.allowMultiple.matcher
|
|
270
|
+
return matched[0];
|
|
271
|
+
}
|
|
293
272
|
|
|
294
273
|
if (isStateful) {
|
|
295
274
|
return state
|
|
@@ -343,27 +343,51 @@ describe('Plugin tests', () => {
|
|
|
343
343
|
type: 'testResource',
|
|
344
344
|
})
|
|
345
345
|
|
|
346
|
-
expect(resourceInfo.allowMultiple
|
|
347
|
-
'path', 'paths'
|
|
348
|
-
])
|
|
346
|
+
expect(resourceInfo.allowMultiple).to.be.true;
|
|
349
347
|
})
|
|
350
348
|
|
|
351
|
-
it('
|
|
349
|
+
it('Can match resources together', async () => {
|
|
352
350
|
const resource = spy(new class extends TestResource {
|
|
353
351
|
getSettings(): ResourceSettings<TestConfig> {
|
|
354
352
|
return {
|
|
355
353
|
...super.getSettings(),
|
|
356
|
-
|
|
354
|
+
parameterSettings: {
|
|
355
|
+
path: { type: 'directory' },
|
|
356
|
+
paths: { type: 'array', itemType: 'directory' }
|
|
357
|
+
},
|
|
358
|
+
allowMultiple: {
|
|
359
|
+
identifyingParameters: ['path', 'paths']
|
|
360
|
+
}
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
363
|
})
|
|
360
364
|
|
|
361
365
|
const testPlugin = Plugin.create('testPlugin', [resource as any]);
|
|
362
366
|
|
|
363
|
-
const
|
|
364
|
-
|
|
367
|
+
const { match } = await testPlugin.match({
|
|
368
|
+
resource: {
|
|
369
|
+
core: { type: 'testResource' },
|
|
370
|
+
parameters: { path: '/my/path', propA: 'abc' },
|
|
371
|
+
},
|
|
372
|
+
array: [
|
|
373
|
+
{
|
|
374
|
+
core: { type: 'testResource' },
|
|
375
|
+
parameters: { path: '/my/other/path', propA: 'abc' },
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
core: { type: 'testResource' },
|
|
379
|
+
parameters: { paths: ['/my/path'], propA: 'def' },
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
core: { type: 'testResource' },
|
|
383
|
+
parameters: { path: '/my/path', propA: 'hig' },
|
|
384
|
+
},
|
|
385
|
+
]
|
|
386
|
+
})
|
|
387
|
+
expect(match).toMatchObject({
|
|
388
|
+
core: { type: 'testResource' },
|
|
389
|
+
parameters: { path: '/my/path', propA: 'hig' },
|
|
365
390
|
})
|
|
366
391
|
|
|
367
|
-
expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([])
|
|
368
392
|
})
|
|
369
393
|
});
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
ImportRequestData,
|
|
7
7
|
ImportResponseData,
|
|
8
8
|
InitializeResponseData,
|
|
9
|
+
MatchRequestData,
|
|
10
|
+
MatchResponseData,
|
|
9
11
|
PlanRequestData,
|
|
10
12
|
PlanResponseData,
|
|
11
13
|
ResourceConfig,
|
|
@@ -73,10 +75,7 @@ export class Plugin {
|
|
|
73
75
|
) as any;
|
|
74
76
|
|
|
75
77
|
const allowMultiple = resource.settings.allowMultiple !== undefined
|
|
76
|
-
|
|
77
|
-
? { identifyingParameters: schema?.required ?? [] }
|
|
78
|
-
: { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] }
|
|
79
|
-
) : undefined
|
|
78
|
+
&& resource.settings.allowMultiple !== false;
|
|
80
79
|
|
|
81
80
|
return {
|
|
82
81
|
plugin: this.name,
|
|
@@ -94,6 +93,34 @@ export class Plugin {
|
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
|
|
96
|
+
async match(data: MatchRequestData): Promise<MatchResponseData> {
|
|
97
|
+
const { resource: resourceConfig, array } = data;
|
|
98
|
+
|
|
99
|
+
const resource = this.resourceControllers.get(resourceConfig.core.type);
|
|
100
|
+
if (!resource) {
|
|
101
|
+
throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parameterMatcher = resource?.parsedSettings.matcher;
|
|
105
|
+
const match = array.find((r) => {
|
|
106
|
+
if (resourceConfig.core.type !== r.core.type) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
|
|
111
|
+
if (resourceConfig.core.name === r.core.name
|
|
112
|
+
&& resourceConfig.core.name
|
|
113
|
+
&& Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parameterMatcher(resourceConfig.parameters, r.parameters);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return { match }
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
async import(data: ImportRequestData): Promise<ImportResponseData> {
|
|
98
125
|
const { core, parameters } = data;
|
|
99
126
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ParameterSetting,
|
|
10
10
|
resolveElementEqualsFn,
|
|
11
11
|
resolveEqualsFn,
|
|
12
|
+
resolveMatcher,
|
|
12
13
|
resolveParameterTransformFn,
|
|
13
14
|
ResourceSettings,
|
|
14
15
|
StatefulParameterSetting
|
|
@@ -38,7 +39,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
38
39
|
id!: string;
|
|
39
40
|
schema?: Partial<JSONSchemaType<T | any>>;
|
|
40
41
|
allowMultiple?: {
|
|
41
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
42
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
42
43
|
requiredParameters?: string[]
|
|
43
44
|
} | boolean;
|
|
44
45
|
|
|
@@ -172,6 +173,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
get matcher(): (desired: Partial<T>, current: Partial<T>) => boolean {
|
|
177
|
+
return resolveMatcher(this);
|
|
178
|
+
}
|
|
179
|
+
|
|
175
180
|
private validateSettings(): void {
|
|
176
181
|
// validate parameter settings
|
|
177
182
|
if (this.settings.parameterSettings) {
|
|
@@ -1054,4 +1054,69 @@ describe('Resource parameter tests', () => {
|
|
|
1054
1054
|
operation: ResourceOperation.NOOP,
|
|
1055
1055
|
})
|
|
1056
1056
|
})
|
|
1057
|
+
|
|
1058
|
+
it('Supports matching using the identfying parameters', async () => {
|
|
1059
|
+
const home = os.homedir()
|
|
1060
|
+
const testPath = path.join(home, 'test/folder');
|
|
1061
|
+
|
|
1062
|
+
const resource = new class extends TestResource {
|
|
1063
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
1064
|
+
return {
|
|
1065
|
+
id: 'resourceType',
|
|
1066
|
+
parameterSettings: {
|
|
1067
|
+
propA: { type: 'array', itemType: 'directory' }
|
|
1068
|
+
},
|
|
1069
|
+
allowMultiple: {
|
|
1070
|
+
identifyingParameters: ['propA']
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const controller = new ResourceController(resource);
|
|
1077
|
+
expect(controller.parsedSettings.matcher({
|
|
1078
|
+
propA: [testPath],
|
|
1079
|
+
propB: 'random1',
|
|
1080
|
+
}, {
|
|
1081
|
+
propA: [testPath],
|
|
1082
|
+
propB: 'random2',
|
|
1083
|
+
})).to.be.true;
|
|
1084
|
+
|
|
1085
|
+
expect(controller.parsedSettings.matcher({
|
|
1086
|
+
propA: [testPath],
|
|
1087
|
+
propB: 'random1',
|
|
1088
|
+
}, {
|
|
1089
|
+
propA: [testPath, testPath],
|
|
1090
|
+
propB: 'random2',
|
|
1091
|
+
})).to.be.false;
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
it('Supports matching using custom matcher', async () => {
|
|
1095
|
+
const home = os.homedir()
|
|
1096
|
+
const testPath = path.join(home, 'test/folder');
|
|
1097
|
+
|
|
1098
|
+
const resource = new class extends TestResource {
|
|
1099
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
1100
|
+
return {
|
|
1101
|
+
id: 'resourceType',
|
|
1102
|
+
parameterSettings: {
|
|
1103
|
+
propA: { type: 'array', itemType: 'directory' }
|
|
1104
|
+
},
|
|
1105
|
+
allowMultiple: {
|
|
1106
|
+
identifyingParameters: ['propA'],
|
|
1107
|
+
matcher: () => false,
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const controller = new ResourceController(resource);
|
|
1114
|
+
expect(controller.parsedSettings.matcher({
|
|
1115
|
+
propA: [testPath],
|
|
1116
|
+
propB: 'random1',
|
|
1117
|
+
}, {
|
|
1118
|
+
propA: [testPath],
|
|
1119
|
+
propB: 'random2',
|
|
1120
|
+
})).to.be.false;
|
|
1121
|
+
})
|
|
1057
1122
|
})
|
|
@@ -53,7 +53,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
53
53
|
*
|
|
54
54
|
* @return The matched resource.
|
|
55
55
|
*/
|
|
56
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
56
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean
|
|
57
57
|
} | boolean
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -408,3 +408,44 @@ export function resolveParameterTransformFn(
|
|
|
408
408
|
|
|
409
409
|
return parameter.transformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined;
|
|
410
410
|
}
|
|
411
|
+
|
|
412
|
+
export function resolveMatcher<T extends StringIndexedObject>(
|
|
413
|
+
settings: ResourceSettings<T>
|
|
414
|
+
): (desired: Partial<T>, current: Partial<T>) => boolean {
|
|
415
|
+
|
|
416
|
+
return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
|
|
417
|
+
? ((desired: Partial<T>, current: Partial<T>) => {
|
|
418
|
+
if (!desired || !current) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const requiredParameters = typeof settings.allowMultiple === 'object'
|
|
423
|
+
? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? []
|
|
424
|
+
: (settings.schema?.required as string[]) ?? []
|
|
425
|
+
|
|
426
|
+
return requiredParameters.every((key) => {
|
|
427
|
+
const currentParameter = current[key];
|
|
428
|
+
const desiredParameter = desired[key];
|
|
429
|
+
|
|
430
|
+
// If both desired and current don't have a certain parameter then we assume they are the same
|
|
431
|
+
if (!currentParameter && !desiredParameter) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!currentParameter) {
|
|
436
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!desiredParameter) {
|
|
441
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const parameterSetting = settings.parameterSettings?.[key];
|
|
446
|
+
const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null
|
|
447
|
+
return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
: settings.allowMultiple.matcher
|
|
451
|
+
}
|