codify-plugin-lib 1.0.144 → 1.0.146

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>;
@@ -42,9 +42,7 @@ export class Plugin {
42
42
  ?? schema?.required
43
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,30 @@ 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
+ if (!resource.settings.allowMultiple) {
68
+ return { match: array.find((r) => r.core.type === resourceConfig.core.type) };
69
+ }
70
+ const parameterMatcher = resource?.parsedSettings.matcher;
71
+ const match = array.find((r) => {
72
+ if (resourceConfig.core.type !== r.core.type) {
73
+ return false;
74
+ }
75
+ // If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
76
+ if (resourceConfig.core.name === r.core.name
77
+ && resourceConfig.core.name
78
+ && Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))) {
79
+ return true;
80
+ }
81
+ return parameterMatcher(resourceConfig.parameters, r.parameters);
82
+ });
83
+ return { match };
84
+ }
63
85
  async import(data) {
64
86
  const { core, parameters } = data;
65
87
  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;
@@ -45,7 +45,6 @@ export class ParsedResourceSettings {
45
45
  ...v,
46
46
  controller: spController,
47
47
  nestedSettings: spController?.parsedSettings,
48
- definition: undefined,
49
48
  };
50
49
  return [k, parsed];
51
50
  }
@@ -101,6 +100,9 @@ export class ParsedResourceSettings {
101
100
  return new Map(resultArray.map((key, idx) => [key, idx]));
102
101
  });
103
102
  }
103
+ get matcher() {
104
+ return resolveMatcher(this);
105
+ }
104
106
  validateSettings() {
105
107
  // validate parameter settings
106
108
  if (this.settings.parameterSettings) {
@@ -111,10 +113,10 @@ export class ParsedResourceSettings {
111
113
  this.validateParameterEqualsFn(v, k);
112
114
  }
113
115
  }
114
- if (this.allowMultiple
115
- && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
116
- throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`);
117
- }
116
+ // if (this.allowMultiple
117
+ // && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
118
+ // throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
119
+ // }
118
120
  const schema = this.settings.schema;
119
121
  if (!this.settings.importAndDestroy && (schema?.oneOf
120
122
  && Array.isArray(schema.oneOf)
@@ -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,37 @@ 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
+ if (!settings.allowMultiple) {
95
+ throw new Error(`Matching only works when allow multiple is enabled. Type: ${settings.id}`);
96
+ }
97
+ const requiredParameters = typeof settings.allowMultiple === 'object'
98
+ ? settings.allowMultiple?.identifyingParameters ?? settings.schema?.required ?? []
99
+ : settings.schema?.required ?? [];
100
+ return requiredParameters.every((key) => {
101
+ const currentParameter = current[key];
102
+ const desiredParameter = desired[key];
103
+ // If both desired and current don't have a certain parameter then we assume they are the same
104
+ if (!currentParameter && !desiredParameter) {
105
+ return true;
106
+ }
107
+ if (!currentParameter) {
108
+ console.warn(`Unable to find required parameter for current ${currentParameter}`);
109
+ return false;
110
+ }
111
+ if (!desiredParameter) {
112
+ console.warn(`Unable to find required parameter for current ${currentParameter}`);
113
+ return false;
114
+ }
115
+ const parameterSetting = settings.parameterSettings?.[key];
116
+ const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null;
117
+ return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
118
+ });
119
+ })
120
+ : settings.allowMultiple.matcher;
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.144",
3
+ "version": "1.0.146",
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
@@ -5,7 +5,7 @@ import { Resource } from '../resource/resource.js';
5
5
  import { Plan } from '../plan/plan.js';
6
6
  import { spy } from 'sinon';
7
7
  import { ResourceSettings } from '../resource/resource-settings.js';
8
- import { TestConfig } from '../utils/test-utils.test.js';
8
+ import { TestConfig, TestStatefulParameter } from '../utils/test-utils.test.js';
9
9
  import { getPty } from '../pty/index.js';
10
10
 
11
11
  interface TestConfig extends StringIndexedObject {
@@ -343,27 +343,109 @@ 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' },
390
+ })
391
+
392
+ const match2 = await testPlugin.match({
393
+ resource: {
394
+ core: { type: 'testResource' },
395
+ parameters: { path: '/my/path', propA: 'abc' },
396
+ },
397
+ array: []
398
+ })
399
+
400
+ expect(match2).toMatchObject({
401
+ match: undefined,
402
+ })
403
+ })
404
+
405
+ it('Can match resources together 2', { timeout: 3000000 }, async () => {
406
+ const resource = spy(new class extends TestResource {
407
+ getSettings(): ResourceSettings<TestConfig> {
408
+ return {
409
+ id: 'ssh-config',
410
+ parameterSettings: {
411
+ hosts: { type: 'stateful', definition: new TestStatefulParameter() }
412
+ },
413
+ importAndDestroy: {
414
+ refreshKeys: ['hosts'],
415
+ defaultRefreshValues: { hosts: [] },
416
+ requiredParameters: []
417
+ },
418
+ dependencies: ['ssh-key'],
419
+ allowMultiple: {
420
+ matcher: (a, b) => a.hosts === b.hosts
421
+ }
422
+ }
423
+ }
424
+ })
425
+
426
+ const testPlugin = Plugin.create('testPlugin', [resource as any]);
427
+
428
+ const { match } = await testPlugin.match({
429
+ resource: {
430
+ core: { type: 'ssh-config' },
431
+ parameters: { hosts: 'a' },
432
+ },
433
+ array: [
434
+ {
435
+ core: { type: 'ssh-config' },
436
+ parameters: { hosts: 'b' },
437
+ },
438
+ {
439
+ core: { type: 'ssh-config' },
440
+ parameters: { hosts: 'a' },
441
+ },
442
+ {
443
+ core: { type: 'ssh-config' },
444
+ parameters: { hosts: 'c' },
445
+ },
446
+ ]
365
447
  })
366
448
 
367
- expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([])
449
+ console.log(match)
368
450
  })
369
451
  });
@@ -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
- ? (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,38 @@ 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
+ if (!resource.settings.allowMultiple) {
105
+ return { match: array.find((r) => r.core.type === resourceConfig.core.type) }
106
+ }
107
+
108
+ const parameterMatcher = resource?.parsedSettings.matcher;
109
+ const match = array.find((r) => {
110
+ if (resourceConfig.core.type !== r.core.type) {
111
+ return false;
112
+ }
113
+
114
+ // If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
115
+ if (resourceConfig.core.name === r.core.name
116
+ && resourceConfig.core.name
117
+ && Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))
118
+ ) {
119
+ return true;
120
+ }
121
+
122
+ return parameterMatcher(resourceConfig.parameters, r.parameters);
123
+ });
124
+
125
+ return { match }
126
+ }
127
+
97
128
  async import(data: ImportRequestData): Promise<ImportResponseData> {
98
129
  const { core, parameters } = data;
99
130
 
@@ -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
 
@@ -91,7 +92,6 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
91
92
  ...v,
92
93
  controller: spController,
93
94
  nestedSettings: spController?.parsedSettings,
94
- definition: undefined,
95
95
  };
96
96
 
97
97
  return [k, parsed as ParsedStatefulParameterSetting];
@@ -172,6 +172,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
172
172
  });
173
173
  }
174
174
 
175
+ get matcher(): (desired: Partial<T>, current: Partial<T>) => boolean {
176
+ return resolveMatcher(this);
177
+ }
178
+
175
179
  private validateSettings(): void {
176
180
  // validate parameter settings
177
181
  if (this.settings.parameterSettings) {
@@ -184,10 +188,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
184
188
  }
185
189
  }
186
190
 
187
- if (this.allowMultiple
188
- && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
189
- throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
190
- }
191
+ // if (this.allowMultiple
192
+ // && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
193
+ // throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
194
+ // }
191
195
 
192
196
  const schema = this.settings.schema as JSONSchemaType<any>;
193
197
  if (!this.settings.importAndDestroy && (schema?.oneOf
@@ -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,47 @@ 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
+ return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
416
+ ? ((desired: Partial<T>, current: Partial<T>) => {
417
+ if (!desired || !current) {
418
+ return false;
419
+ }
420
+
421
+ if (!settings.allowMultiple) {
422
+ throw new Error(`Matching only works when allow multiple is enabled. Type: ${settings.id}`)
423
+ }
424
+
425
+ const requiredParameters = typeof settings.allowMultiple === 'object'
426
+ ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? []
427
+ : (settings.schema?.required as string[]) ?? []
428
+
429
+ return requiredParameters.every((key) => {
430
+ const currentParameter = current[key];
431
+ const desiredParameter = desired[key];
432
+
433
+ // If both desired and current don't have a certain parameter then we assume they are the same
434
+ if (!currentParameter && !desiredParameter) {
435
+ return true;
436
+ }
437
+
438
+ if (!currentParameter) {
439
+ console.warn(`Unable to find required parameter for current ${currentParameter}`)
440
+ return false;
441
+ }
442
+
443
+ if (!desiredParameter) {
444
+ console.warn(`Unable to find required parameter for current ${currentParameter}`)
445
+ return false;
446
+ }
447
+
448
+ const parameterSetting = settings.parameterSettings?.[key];
449
+ const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null
450
+ return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
451
+ })
452
+ })
453
+ : settings.allowMultiple.matcher
454
+ }