codify-plugin-lib 1.0.143 → 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.
@@ -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 = typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple.matcher
160
- ? ((desired, currentArr) => {
161
- const requiredParameters = typeof settings.allowMultiple === 'object'
162
- ? settings.allowMultiple?.identifyingParameters ?? settings.schema?.required ?? []
163
- : settings.schema?.required ?? [];
164
- const matched = currentArr.filter((c) => requiredParameters.every((key) => {
165
- const currentParameter = c[key];
166
- const desiredParameter = desired[key];
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
@@ -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>;
@@ -40,11 +40,9 @@ export class Plugin {
40
40
  const schema = resource.settings.schema;
41
41
  const requiredPropertyNames = (resource.settings.importAndDestroy?.requiredParameters
42
42
  ?? schema?.required
43
- ?? null);
43
+ ?? undefined);
44
44
  const allowMultiple = resource.settings.allowMultiple !== undefined
45
- ? (typeof resource.settings.allowMultiple === 'boolean'
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>[]) => 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>[]) => 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.143",
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.70",
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",
@@ -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: ResourceSettings<T>,
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 = typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple.matcher
264
- ? ((desired: Partial<T>, currentArr: Array<Partial<T>>) => {
265
- const requiredParameters = typeof settings.allowMultiple === 'object'
266
- ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? []
267
- : (settings.schema?.required as string[]) ?? []
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
- return matched[0];
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?.requiredParameters).toMatchObject([
347
- 'path', 'paths'
348
- ])
346
+ expect(resourceInfo.allowMultiple).to.be.true;
349
347
  })
350
348
 
351
- it('Returns an empty array by default for allowMultiple for getResourceInfo', async () => {
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
- allowMultiple: true
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 resourceInfo = await testPlugin.getResourceInfo({
364
- type: 'testResource',
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
  });
@@ -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,
@@ -69,14 +71,11 @@ export class Plugin {
69
71
  const requiredPropertyNames = (
70
72
  resource.settings.importAndDestroy?.requiredParameters
71
73
  ?? schema?.required
72
- ?? null
73
- ) as null | string[];
74
+ ?? undefined
75
+ ) as any;
74
76
 
75
77
  const allowMultiple = resource.settings.allowMultiple !== undefined
76
- ? (typeof resource.settings.allowMultiple === 'boolean'
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>[]) => 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>[],) => 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
+ }