codify-plugin-lib 1.0.131 → 1.0.133

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.
@@ -7,7 +7,7 @@ on: [ push ]
7
7
 
8
8
  jobs:
9
9
  build-and-test:
10
- runs-on: ubuntu-latest
10
+ runs-on: macos-latest
11
11
  steps:
12
12
  - uses: actions/checkout@v4
13
13
  - uses: actions/setup-node@v4
@@ -15,5 +15,4 @@ jobs:
15
15
  node-version: '20.x'
16
16
  cache: 'npm'
17
17
  - run: npm ci
18
- - run: tsc
19
18
  - run: npm run test
@@ -61,8 +61,6 @@ export declare class Plan<T extends StringIndexedObject> {
61
61
  */
62
62
  private static filterCurrentParams;
63
63
  requiresChanges(): boolean;
64
- /**
65
- * Convert the plan to a JSON response object
66
- */
64
+ /** Convert the plan to a JSON response object */
67
65
  toResponse(): PlanResponseData;
68
66
  }
package/dist/plan/plan.js CHANGED
@@ -156,12 +156,36 @@ 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?.requiredParameters ?? 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
183
  if (isStateful) {
160
184
  return state
161
- ? settings.allowMultiple.matcher(state, currentArray)
185
+ ? matcher(state, currentArray) ?? null
162
186
  : null;
163
187
  }
164
- return settings.allowMultiple.matcher(desired, currentArray);
188
+ return matcher(desired, currentArray) ?? null;
165
189
  }
166
190
  /**
167
191
  * Only keep relevant params for the plan. We don't want to change settings that were not already
@@ -241,13 +265,13 @@ export class Plan {
241
265
  const currentCopy = [...v];
242
266
  const defaultFilterMethod = ((desired, current) => {
243
267
  const result = [];
244
- for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
245
- const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2));
268
+ for (let counter = desired.length - 1; counter >= 0; counter--) {
269
+ const idx = currentCopy.findIndex((e2) => matcher(desired[counter], e2));
246
270
  if (idx === -1) {
247
271
  continue;
248
272
  }
249
- desiredCopy.splice(counter, 1);
250
- const [element] = currentCopy.splice(idx, 1);
273
+ desired.splice(counter, 1);
274
+ const [element] = current.splice(idx, 1);
251
275
  result.push(element);
252
276
  }
253
277
  return result;
@@ -263,9 +287,7 @@ export class Plan {
263
287
  requiresChanges() {
264
288
  return this.changeSet.operation !== ResourceOperation.NOOP;
265
289
  }
266
- /**
267
- * Convert the plan to a JSON response object
268
- */
290
+ /** Convert the plan to a JSON response object */
269
291
  toResponse() {
270
292
  return {
271
293
  planId: this.id,
@@ -38,7 +38,7 @@ export class Plugin {
38
38
  }
39
39
  const resource = this.resourceControllers.get(data.type);
40
40
  const schema = resource.settings.schema;
41
- const requiredPropertyNames = (resource.settings.import?.requiredParameters
41
+ const requiredPropertyNames = (resource.settings.importAndDestroy?.requiredParameters
42
42
  ?? schema?.required
43
43
  ?? null);
44
44
  return {
@@ -1,3 +1,4 @@
1
+ import { JSONSchemaType } from 'ajv';
1
2
  import { StringIndexedObject } from 'codify-schemas';
2
3
  import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
3
4
  import { ArrayParameterSetting, DefaultParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
@@ -17,10 +18,11 @@ export type ParsedParameterSetting = {
17
18
  export declare class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
18
19
  private cache;
19
20
  id: string;
20
- schema?: unknown;
21
+ schema?: JSONSchemaType<T | any>;
21
22
  allowMultiple?: {
22
- matcher: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
23
- } | undefined;
23
+ matcher?: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
24
+ requiredParameters?: string[];
25
+ } | boolean;
24
26
  removeStatefulParametersBeforeDestroy?: boolean | undefined;
25
27
  dependencies?: string[] | undefined;
26
28
  inputTransformation?: ((desired: Partial<T>) => unknown) | undefined;
@@ -1,5 +1,5 @@
1
1
  import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
2
- import { resolveEqualsFn, resolveFnFromEqualsFnOrString, resolveParameterTransformFn } from './resource-settings.js';
2
+ import { resolveElementEqualsFn, resolveEqualsFn, resolveParameterTransformFn } from './resource-settings.js';
3
3
  export class ParsedResourceSettings {
4
4
  cache = new Map();
5
5
  id;
@@ -52,8 +52,7 @@ export class ParsedResourceSettings {
52
52
  if (v.type === 'array') {
53
53
  const parsed = {
54
54
  ...v,
55
- isElementEqual: resolveFnFromEqualsFnOrString(v.isElementEqual)
56
- ?? ((a, b) => a === b),
55
+ isElementEqual: resolveElementEqualsFn(v)
57
56
  };
58
57
  return [k, parsed];
59
58
  }
@@ -84,7 +83,7 @@ export class ParsedResourceSettings {
84
83
  }
85
84
  return Object.fromEntries(Object.entries(this.settings.parameterSettings)
86
85
  .filter(([_, v]) => resolveParameterTransformFn(v) !== undefined)
87
- .map(([k, v]) => [k, resolveParameterTransformFn(v)]));
86
+ .map(([k, v]) => [k, resolveParameterTransformFn(v).to]));
88
87
  });
89
88
  }
90
89
  get statefulParameterOrder() {
@@ -117,7 +116,7 @@ export class ParsedResourceSettings {
117
116
  throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`);
118
117
  }
119
118
  const schema = this.settings.schema;
120
- if (!this.settings.import && (schema?.oneOf
119
+ if (!this.settings.importAndDestroy && (schema?.oneOf
121
120
  && Array.isArray(schema.oneOf)
122
121
  && schema.oneOf.some((s) => s.required))
123
122
  || (schema?.anyOf
@@ -137,8 +136,8 @@ export class ParsedResourceSettings {
137
136
  'determine the prompt to ask users during imports. It can\'t parse which parameters are needed when ' +
138
137
  'required is declared conditionally.');
139
138
  }
140
- if (this.settings.import) {
141
- const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.import;
139
+ if (this.settings.importAndDestroy) {
140
+ const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.importAndDestroy;
142
141
  const requiredParametersNotInSchema = requiredParameters
143
142
  ?.filter((p) => schema && !(schema.properties[p]));
144
143
  if (schema && requiredParametersNotInSchema && requiredParametersNotInSchema.length > 0) {
@@ -16,6 +16,7 @@ export declare class ResourceController<T extends StringIndexedObject> {
16
16
  initialize(): Promise<void>;
17
17
  validate(core: ResourceConfig, parameters: Partial<T>): Promise<ValidateResponseData['resourceValidations'][0]>;
18
18
  plan(core: ResourceConfig, desired: Partial<T> | null, state: Partial<T> | null, isStateful?: boolean): Promise<Plan<T>>;
19
+ planDestroy(core: ResourceConfig, parameters: Partial<T>): Promise<Plan<T>>;
19
20
  apply(plan: Plan<T>): Promise<void>;
20
21
  import(core: ResourceConfig, parameters: Partial<T>): Promise<Array<ResourceJson> | null>;
21
22
  private applyCreate;
@@ -31,11 +31,12 @@ export class ResourceController {
31
31
  return this.resource.initialize();
32
32
  }
33
33
  async validate(core, parameters) {
34
+ const originalParameters = structuredClone(parameters);
34
35
  await this.applyTransformParameters(parameters);
35
36
  this.addDefaultValues(parameters);
36
37
  if (this.schemaValidator) {
37
38
  // Schema validator uses pre transformation parameters
38
- const isValid = this.schemaValidator(parameters);
39
+ const isValid = this.schemaValidator(originalParameters);
39
40
  if (!isValid) {
40
41
  return {
41
42
  isValid: false,
@@ -108,6 +109,23 @@ export class ResourceController {
108
109
  isStateful
109
110
  });
110
111
  }
112
+ async planDestroy(core, parameters) {
113
+ this.addDefaultValues(parameters);
114
+ await this.applyTransformParameters(parameters);
115
+ // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
116
+ const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
117
+ ? {
118
+ ...Object.fromEntries(this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])),
119
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
120
+ ...parameters,
121
+ }
122
+ : {
123
+ ...Object.fromEntries(this.getAllParameterKeys().map((k) => [k, null])),
124
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
125
+ ...parameters,
126
+ };
127
+ return this.plan(core, null, parametersToRefresh, true);
128
+ }
111
129
  async apply(plan) {
112
130
  if (plan.getResourceType() !== this.typeId) {
113
131
  throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
@@ -132,15 +150,15 @@ export class ResourceController {
132
150
  this.addDefaultValues(parameters);
133
151
  await this.applyTransformParameters(parameters);
134
152
  // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
135
- const parametersToRefresh = this.settings.import?.refreshKeys
153
+ const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
136
154
  ? {
137
- ...Object.fromEntries(this.settings.import?.refreshKeys.map((k) => [k, null])),
138
- ...this.settings.import?.defaultRefreshValues,
155
+ ...Object.fromEntries(this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])),
156
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
139
157
  ...parameters,
140
158
  }
141
159
  : {
142
160
  ...Object.fromEntries(this.getAllParameterKeys().map((k) => [k, null])),
143
- ...this.settings.import?.defaultRefreshValues,
161
+ ...this.settings.importAndDestroy?.defaultRefreshValues,
144
162
  ...parameters,
145
163
  };
146
164
  // Parse data from the user supplied config
@@ -1,5 +1,10 @@
1
+ import { JSONSchemaType } from 'ajv';
1
2
  import { StringIndexedObject } from 'codify-schemas';
2
3
  import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
4
+ export interface InputTransformation {
5
+ to: (input: any) => Promise<any> | any;
6
+ from: (current: any) => Promise<any> | any;
7
+ }
3
8
  /**
4
9
  * The configuration and settings for a resource.
5
10
  */
@@ -11,13 +16,23 @@ export interface ResourceSettings<T extends StringIndexedObject> {
11
16
  /**
12
17
  * Schema to validate user configs with. Must be in the format JSON Schema draft07
13
18
  */
14
- schema?: unknown;
19
+ schema?: JSONSchemaType<T | any>;
15
20
  /**
16
21
  * Allow multiple of the same resource to unique. Set truthy if
17
22
  * multiples are allowed, for example for applications, there can be multiple copy of the same application installed
18
23
  * on the system. Or there can be multiple git repos. Defaults to false.
19
24
  */
20
25
  allowMultiple?: {
26
+ /**
27
+ * A set of parameters that uniquely identifies a resource. The value of these parameters is used to determine which
28
+ * resource is which when multiple can exist at the same time. Defaults to the required parameters inside the json
29
+ * schema.
30
+ *
31
+ * For example:
32
+ * If paramA is required, then if resource1.paramA === resource2.paramA then are the same resource.
33
+ * If resource1.paramA !== resource1.paramA, then they are different.
34
+ */
35
+ requiredParameters?: string[];
21
36
  /**
22
37
  * If multiple copies are allowed then a matcher must be defined to match the desired
23
38
  * config with one of the resources currently existing on the system. Return null if there is no match.
@@ -27,8 +42,8 @@ export interface ResourceSettings<T extends StringIndexedObject> {
27
42
  *
28
43
  * @return The matched resource.
29
44
  */
30
- matcher: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
31
- };
45
+ matcher?: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
46
+ } | boolean;
32
47
  /**
33
48
  * If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
34
49
  * if the stateful parameter needs to be first uninstalled (cleanup) before the overall resource can be
@@ -53,9 +68,9 @@ export interface ResourceSettings<T extends StringIndexedObject> {
53
68
  */
54
69
  inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
55
70
  /**
56
- * Customize the import behavior of the resource. By default, <code>codify import</code> will call `refresh()` with
57
- * every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
58
- * in the schema and will prompt the user for these values before performing the import.
71
+ * Customize the import and destory behavior of the resource. By default, <code>codify import</code> and <code>codify destroy</code> will call
72
+ * `refresh()` with every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
73
+ * in the schema and will prompt the user for these values before performing the import or destroy.
59
74
  *
60
75
  * <b>Example:</b><br>
61
76
  * Resource `alias` with parameters
@@ -71,7 +86,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
71
86
  * { type: 'alias', alias: 'user-input', value: 'git push' }
72
87
  * ```
73
88
  */
74
- import?: {
89
+ importAndDestroy?: {
75
90
  /**
76
91
  * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
77
92
  * from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in
@@ -81,7 +96,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
81
96
  * the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
82
97
  * chooses which alias the resource is managing).
83
98
  *
84
- * See {@link import} for more information on how importing works.
99
+ * See {@link importAndDestroy} for more information on how importing works.
85
100
  */
86
101
  requiredParameters?: Array<Partial<keyof T>>;
87
102
  /**
@@ -91,13 +106,13 @@ export interface ResourceSettings<T extends StringIndexedObject> {
91
106
  * By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
92
107
  * in value can be customized using {@link defaultRefreshValues}
93
108
  *
94
- * See {@link import} for more information on how importing works.
109
+ * See {@link importAndDestroy} for more information on how importing works.
95
110
  */
96
111
  refreshKeys?: Array<Partial<keyof T>>;
97
112
  /**
98
113
  * Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
99
114
  *
100
- * See {@link import} for more information on how importing works.
115
+ * See {@link importAndDestroy} for more information on how importing works.
101
116
  */
102
117
  defaultRefreshValues?: Partial<T>;
103
118
  };
@@ -130,12 +145,13 @@ export interface DefaultParameterSetting {
130
145
  */
131
146
  default?: unknown;
132
147
  /**
133
- * A transformation of the input value for this parameter. This transformation is only applied to the desired parameter
134
- * value supplied by the user.
148
+ * A transformation of the input value for this parameter. Two transformations need to be provided: to (from desired to
149
+ * the internal type), and from (from the internal type back to desired). All transformations need to be bi-directional
150
+ * to support imports properly
135
151
  *
136
152
  * @param input The original parameter value from the desired config.
137
153
  */
138
- inputTransformation?: (input: any) => Promise<any> | any;
154
+ inputTransformation?: InputTransformation;
139
155
  /**
140
156
  * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
141
157
  * This value will override the pre-set equality function from the type. Return true if the desired value is
@@ -192,6 +208,11 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
192
208
  * Defaults to true.
193
209
  */
194
210
  filterInStatelessMode?: ((desired: any[], current: any[]) => any[]) | boolean;
211
+ /**
212
+ * The type of the array item. See {@link ParameterSettingType} for the available options. This value
213
+ * is mainly used to determine the equality method when performing diffing.
214
+ */
215
+ itemType?: ParameterSettingType;
195
216
  }
196
217
  /**
197
218
  * Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
@@ -215,5 +236,6 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
215
236
  order?: number;
216
237
  }
217
238
  export declare function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, current: unknown) => boolean;
239
+ export declare function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean;
218
240
  export declare function resolveFnFromEqualsFnOrString(fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined): ((a: unknown, b: unknown) => boolean) | undefined;
219
- export declare function resolveParameterTransformFn(parameter: ParameterSetting): ((input: any, parameter: ParameterSetting) => Promise<any> | any) | undefined;
241
+ export declare function resolveParameterTransformFn(parameter: ParameterSetting): InputTransformation | undefined;
@@ -1,6 +1,6 @@
1
1
  import isObjectsEqual from 'lodash.isequal';
2
2
  import path from 'node:path';
3
- import { areArraysEqual, untildify } from '../utils/utils.js';
3
+ import { areArraysEqual, tildify, untildify } from '../utils/utils.js';
4
4
  const ParameterEqualsDefaults = {
5
5
  'boolean': (a, b) => Boolean(a) === Boolean(b),
6
6
  'directory': (a, b) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
@@ -13,15 +13,25 @@ const ParameterEqualsDefaults = {
13
13
  export function resolveEqualsFn(parameter) {
14
14
  const isEqual = resolveFnFromEqualsFnOrString(parameter.isEqual);
15
15
  if (parameter.type === 'array') {
16
- const arrayParameter = parameter;
17
- const isElementEqual = resolveFnFromEqualsFnOrString(arrayParameter.isElementEqual);
18
- return isEqual ?? areArraysEqual.bind(areArraysEqual, isElementEqual);
16
+ return isEqual ?? areArraysEqual.bind(areArraysEqual, resolveElementEqualsFn(parameter));
19
17
  }
20
18
  if (parameter.type === 'stateful') {
21
19
  return resolveEqualsFn(parameter.definition.getSettings());
22
20
  }
23
21
  return isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
24
22
  }
23
+ export function resolveElementEqualsFn(parameter) {
24
+ if (parameter.isElementEqual) {
25
+ const elementEq = resolveFnFromEqualsFnOrString(parameter.isElementEqual);
26
+ if (elementEq) {
27
+ return elementEq;
28
+ }
29
+ }
30
+ if (parameter.itemType && ParameterEqualsDefaults[parameter.itemType]) {
31
+ return ParameterEqualsDefaults[parameter.itemType];
32
+ }
33
+ return (a, b) => a === b;
34
+ }
25
35
  // This resolves the fn if it is a string.
26
36
  // A string can be specified to use a default equals method
27
37
  export function resolveFnFromEqualsFnOrString(fnOrString) {
@@ -34,15 +44,37 @@ export function resolveFnFromEqualsFnOrString(fnOrString) {
34
44
  return fnOrString;
35
45
  }
36
46
  const ParameterTransformationDefaults = {
37
- 'directory': (a) => path.resolve(untildify(String(a))),
38
- 'stateful': (a, b) => {
39
- const sp = b;
40
- return (sp.definition?.getSettings()?.inputTransformation)
41
- ? (sp.definition.getSettings().inputTransformation(a))
42
- : a;
47
+ 'directory': {
48
+ to: (a) => path.resolve(untildify(String(a))),
49
+ from: (a) => tildify(String(a)),
43
50
  },
44
- 'string': String,
51
+ 'string': {
52
+ to: String,
53
+ from: String,
54
+ }
45
55
  };
46
56
  export function resolveParameterTransformFn(parameter) {
57
+ if (parameter.type === 'stateful' && !parameter.inputTransformation) {
58
+ const sp = parameter.definition.getSettings();
59
+ if (sp.inputTransformation) {
60
+ return parameter.definition?.getSettings()?.inputTransformation;
61
+ }
62
+ return sp.type ? ParameterTransformationDefaults[sp.type] : undefined;
63
+ }
64
+ if (parameter.type === 'array'
65
+ && parameter.itemType
66
+ && ParameterTransformationDefaults[parameter.itemType]
67
+ && !parameter.inputTransformation) {
68
+ const itemType = parameter.itemType;
69
+ const itemTransformation = ParameterTransformationDefaults[itemType];
70
+ return {
71
+ to(input) {
72
+ return input.map((i) => itemTransformation.to(i));
73
+ },
74
+ from(input) {
75
+ return input.map((i) => itemTransformation.from(i));
76
+ }
77
+ };
78
+ }
47
79
  return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type] ?? undefined;
48
80
  }
@@ -1,4 +1,4 @@
1
- import { resolveEqualsFn, resolveFnFromEqualsFnOrString } from '../resource/resource-settings.js';
1
+ import { resolveElementEqualsFn, resolveEqualsFn, } from '../resource/resource-settings.js';
2
2
  /**
3
3
  * This class is analogous to what {@link ResourceController} is for {@link Resource}
4
4
  * It's a bit messy because this class supports both {@link StatefulParameter} and {@link ArrayStatefulParameter}
@@ -15,8 +15,7 @@ export class StatefulParameterController {
15
15
  this.parsedSettings = (this.isArrayStatefulParameter || this.settings.type === 'array') ? {
16
16
  ...this.settings,
17
17
  isEqual: resolveEqualsFn(this.settings),
18
- isElementEqual: resolveFnFromEqualsFnOrString(this.settings.isElementEqual)
19
- ?? ((a, b) => a === b)
18
+ isElementEqual: resolveElementEqualsFn(this.settings)
20
19
  } : {
21
20
  ...this.settings,
22
21
  isEqual: resolveEqualsFn(this.settings),
@@ -6,4 +6,5 @@ export declare function splitUserConfig<T extends StringIndexedObject>(config: R
6
6
  };
7
7
  export declare function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean;
8
8
  export declare function untildify(pathWithTilde: string): string;
9
+ export declare function tildify(pathWithTilde: string): string;
9
10
  export declare function areArraysEqual(isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined, desired: unknown, current: unknown): boolean;
@@ -22,6 +22,9 @@ const homeDirectory = os.homedir();
22
22
  export function untildify(pathWithTilde) {
23
23
  return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
24
24
  }
25
+ export function tildify(pathWithTilde) {
26
+ return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde;
27
+ }
25
28
  export function areArraysEqual(isElementEqual, desired, current) {
26
29
  if (!desired || !current) {
27
30
  return false;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.131",
3
+ "version": "1.0.133",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "test": "vitest",
10
+ "posttest": "tsc",
10
11
  "prepublishOnly": "tsc"
11
12
  },
12
13
  "keywords": [],
@@ -33,7 +34,7 @@
33
34
  "@types/uuid": "^10.0.0",
34
35
  "@types/lodash.isequal": "^4.5.8",
35
36
  "chai-as-promised": "^7.1.1",
36
- "vitest": "^1.4.0",
37
+ "vitest": "^3.0.5",
37
38
  "vitest-mock-extended": "^1.3.1",
38
39
  "sinon": "^17.0.1",
39
40
  "eslint": "^8.51.0",
@@ -230,6 +230,120 @@ describe('Plan entity tests', () => {
230
230
  ])
231
231
  })
232
232
  })
233
+
234
+ it('Can use the requiredParameters to match the correct resources together', async () => {
235
+ const resource1 = new class extends TestResource {
236
+ getSettings(): ResourceSettings<TestConfig> {
237
+ return {
238
+ id: 'type',
239
+ parameterSettings: {
240
+ propA: { type: 'string' },
241
+ propB: { type: 'string', canModify: true },
242
+ },
243
+ allowMultiple: {
244
+ requiredParameters: ['propA']
245
+ }
246
+ }
247
+ }
248
+
249
+ async refresh(): Promise<Partial<any> | null> {
250
+ return [{
251
+ propA: 'same',
252
+ propB: 'old',
253
+ }, {
254
+ propA: 'different',
255
+ propB: 'different',
256
+ }]
257
+ }
258
+ }
259
+
260
+ const controller = new ResourceController(resource1);
261
+ const plan = await controller.plan(
262
+ { type: 'type' },
263
+ { propA: 'same', propB: 'new' },
264
+ null,
265
+ false
266
+ )
267
+
268
+ expect(plan.changeSet).toMatchObject({
269
+ operation: ResourceOperation.MODIFY,
270
+ parameterChanges: expect.arrayContaining([
271
+ expect.objectContaining({
272
+ name: 'propA',
273
+ previousValue: 'same',
274
+ newValue: 'same',
275
+ operation: 'noop'
276
+ }),
277
+ expect.objectContaining({
278
+ name: 'propB',
279
+ previousValue: 'old',
280
+ newValue: 'new',
281
+ operation: 'modify'
282
+ })
283
+ ])
284
+ })
285
+ })
286
+
287
+ it('Can use the schema to determine required parameters for multiple allowed', async () => {
288
+ const resource1 = new class extends TestResource {
289
+ getSettings(): ResourceSettings<TestConfig> {
290
+ return {
291
+ id: 'type',
292
+ parameterSettings: {
293
+ propA: { type: 'string' },
294
+ propB: { type: 'string', canModify: true },
295
+ },
296
+ allowMultiple: true,
297
+ schema: {
298
+ '$schema': 'http://json-schema.org/draft-07/schema',
299
+ '$id': 'https://www.codifycli.com/type.json',
300
+ 'type': 'object',
301
+ 'properties': {
302
+ propA: { type: 'string' },
303
+ propB: { type: 'string' }
304
+ },
305
+ required: ['propA']
306
+ }
307
+ }
308
+ }
309
+
310
+ async refresh(): Promise<Partial<any> | null> {
311
+ return [{
312
+ propA: 'same',
313
+ propB: 'old',
314
+ }, {
315
+ propA: 'different',
316
+ propB: 'different',
317
+ }]
318
+ }
319
+ }
320
+
321
+ const controller = new ResourceController(resource1);
322
+ const plan = await controller.plan(
323
+ { type: 'type' },
324
+ { propA: 'same', propB: 'new' },
325
+ null,
326
+ false
327
+ )
328
+
329
+ expect(plan.changeSet).toMatchObject({
330
+ operation: ResourceOperation.MODIFY,
331
+ parameterChanges: expect.arrayContaining([
332
+ expect.objectContaining({
333
+ name: 'propA',
334
+ previousValue: 'same',
335
+ newValue: 'same',
336
+ operation: 'noop'
337
+ }),
338
+ expect.objectContaining({
339
+ name: 'propB',
340
+ previousValue: 'old',
341
+ newValue: 'new',
342
+ operation: 'modify'
343
+ })
344
+ ])
345
+ })
346
+ })
233
347
  })
234
348
 
235
349
  function createTestResource() {