codify-plugin-lib 1.0.132 → 1.0.134
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/plan/plan.d.ts +1 -3
- package/dist/plan/plan.js +31 -9
- package/dist/plugin/plugin.js +1 -1
- package/dist/resource/parsed-resource-settings.d.ts +5 -3
- package/dist/resource/parsed-resource-settings.js +6 -7
- package/dist/resource/resource-controller.d.ts +1 -0
- package/dist/resource/resource-controller.js +21 -4
- package/dist/resource/resource-settings.d.ts +36 -14
- package/dist/resource/resource-settings.js +43 -11
- package/dist/stateful-parameter/stateful-parameter-controller.js +2 -3
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.js +3 -0
- package/package.json +2 -2
- package/src/plan/plan.test.ts +114 -0
- package/src/plan/plan.ts +38 -9
- package/src/plugin/plugin.test.ts +2 -3
- package/src/plugin/plugin.ts +1 -1
- package/src/resource/parsed-resource-settings.test.ts +4 -4
- package/src/resource/parsed-resource-settings.ts +12 -9
- package/src/resource/resource-controller.ts +31 -4
- package/src/resource/resource-settings.test.ts +87 -14
- package/src/resource/resource-settings.ts +91 -27
- package/src/stateful-parameter/stateful-parameter-controller.ts +2 -3
- package/src/utils/utils.ts +4 -0
package/dist/plan/plan.d.ts
CHANGED
|
@@ -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
|
-
?
|
|
185
|
+
? matcher(state, currentArray) ?? null
|
|
162
186
|
: null;
|
|
163
187
|
}
|
|
164
|
-
return
|
|
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 =
|
|
245
|
-
const idx = currentCopy.findIndex((e2) => matcher(
|
|
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
|
-
|
|
250
|
-
const [element] =
|
|
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,
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -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.
|
|
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?:
|
|
21
|
+
schema?: Partial<JSONSchemaType<T | any>>;
|
|
21
22
|
allowMultiple?: {
|
|
22
|
-
matcher
|
|
23
|
-
|
|
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 {
|
|
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:
|
|
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.
|
|
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.
|
|
141
|
-
const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.
|
|
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;
|
|
@@ -109,6 +109,23 @@ export class ResourceController {
|
|
|
109
109
|
isStateful
|
|
110
110
|
});
|
|
111
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
|
+
}
|
|
112
129
|
async apply(plan) {
|
|
113
130
|
if (plan.getResourceType() !== this.typeId) {
|
|
114
131
|
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
@@ -133,15 +150,15 @@ export class ResourceController {
|
|
|
133
150
|
this.addDefaultValues(parameters);
|
|
134
151
|
await this.applyTransformParameters(parameters);
|
|
135
152
|
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
136
|
-
const parametersToRefresh = this.settings.
|
|
153
|
+
const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
|
|
137
154
|
? {
|
|
138
|
-
...Object.fromEntries(this.settings.
|
|
139
|
-
...this.settings.
|
|
155
|
+
...Object.fromEntries(this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])),
|
|
156
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
140
157
|
...parameters,
|
|
141
158
|
}
|
|
142
159
|
: {
|
|
143
160
|
...Object.fromEntries(this.getAllParameterKeys().map((k) => [k, null])),
|
|
144
|
-
...this.settings.
|
|
161
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
145
162
|
...parameters,
|
|
146
163
|
};
|
|
147
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?:
|
|
19
|
+
schema?: Partial<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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
134
|
-
*
|
|
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?:
|
|
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):
|
|
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
|
-
|
|
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':
|
|
38
|
-
|
|
39
|
-
|
|
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':
|
|
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,
|
|
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:
|
|
19
|
-
?? ((a, b) => a === b)
|
|
18
|
+
isElementEqual: resolveElementEqualsFn(this.settings)
|
|
20
19
|
} : {
|
|
21
20
|
...this.settings,
|
|
22
21
|
isEqual: resolveEqualsFn(this.settings),
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/utils.js
CHANGED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.134",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@types/uuid": "^10.0.0",
|
|
35
35
|
"@types/lodash.isequal": "^4.5.8",
|
|
36
36
|
"chai-as-promised": "^7.1.1",
|
|
37
|
-
"vitest": "^
|
|
37
|
+
"vitest": "^3.0.5",
|
|
38
38
|
"vitest-mock-extended": "^1.3.1",
|
|
39
39
|
"sinon": "^17.0.1",
|
|
40
40
|
"eslint": "^8.51.0",
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -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() {
|
package/src/plan/plan.ts
CHANGED
|
@@ -260,13 +260,44 @@ 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?.requiredParameters ?? (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
|
+
}
|
|
289
|
+
|
|
290
|
+
return matched[0];
|
|
291
|
+
})
|
|
292
|
+
: settings.allowMultiple.matcher
|
|
293
|
+
|
|
263
294
|
if (isStateful) {
|
|
264
295
|
return state
|
|
265
|
-
?
|
|
296
|
+
? matcher(state, currentArray) ?? null
|
|
266
297
|
: null
|
|
267
298
|
}
|
|
268
299
|
|
|
269
|
-
return
|
|
300
|
+
return matcher(desired!, currentArray) ?? null;
|
|
270
301
|
}
|
|
271
302
|
|
|
272
303
|
/**
|
|
@@ -384,15 +415,15 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
384
415
|
const defaultFilterMethod = ((desired: any[], current: any[]) => {
|
|
385
416
|
const result = [];
|
|
386
417
|
|
|
387
|
-
for (let counter =
|
|
388
|
-
const idx = currentCopy.findIndex((e2) => matcher(
|
|
418
|
+
for (let counter = desired.length - 1; counter >= 0; counter--) {
|
|
419
|
+
const idx = currentCopy.findIndex((e2) => matcher(desired[counter], e2))
|
|
389
420
|
|
|
390
421
|
if (idx === -1) {
|
|
391
422
|
continue;
|
|
392
423
|
}
|
|
393
424
|
|
|
394
|
-
|
|
395
|
-
const [element] =
|
|
425
|
+
desired.splice(counter, 1)
|
|
426
|
+
const [element] = current.splice(idx, 1)
|
|
396
427
|
result.push(element)
|
|
397
428
|
}
|
|
398
429
|
|
|
@@ -413,9 +444,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
413
444
|
return this.changeSet.operation !== ResourceOperation.NOOP;
|
|
414
445
|
}
|
|
415
446
|
|
|
416
|
-
/**
|
|
417
|
-
* Convert the plan to a JSON response object
|
|
418
|
-
*/
|
|
447
|
+
/** Convert the plan to a JSON response object */
|
|
419
448
|
toResponse(): PlanResponseData {
|
|
420
449
|
return {
|
|
421
450
|
planId: this.id,
|
|
@@ -6,7 +6,6 @@ import { Plan } from '../plan/plan.js';
|
|
|
6
6
|
import { spy } from 'sinon';
|
|
7
7
|
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
8
8
|
import { TestConfig } from '../utils/test-utils.test.js';
|
|
9
|
-
import { ApplyValidationError } from '../common/errors.js';
|
|
10
9
|
import { getPty } from '../pty/index.js';
|
|
11
10
|
|
|
12
11
|
interface TestConfig extends StringIndexedObject {
|
|
@@ -194,7 +193,7 @@ describe('Plugin tests', () => {
|
|
|
194
193
|
return {
|
|
195
194
|
id: 'typeId',
|
|
196
195
|
schema,
|
|
197
|
-
|
|
196
|
+
importAndDestroy: {
|
|
198
197
|
requiredParameters: []
|
|
199
198
|
}
|
|
200
199
|
}
|
|
@@ -229,7 +228,7 @@ describe('Plugin tests', () => {
|
|
|
229
228
|
|
|
230
229
|
await expect(() => testPlugin.apply({ plan }))
|
|
231
230
|
.rejects
|
|
232
|
-
.toThrowError(
|
|
231
|
+
.toThrowError();
|
|
233
232
|
expect(resource.modify.calledOnce).to.be.true;
|
|
234
233
|
})
|
|
235
234
|
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -67,7 +67,7 @@ export class Plugin {
|
|
|
67
67
|
|
|
68
68
|
const schema = resource.settings.schema as JSONSchemaType<any> | undefined;
|
|
69
69
|
const requiredPropertyNames = (
|
|
70
|
-
resource.settings.
|
|
70
|
+
resource.settings.importAndDestroy?.requiredParameters
|
|
71
71
|
?? schema?.required
|
|
72
72
|
?? null
|
|
73
73
|
) as null | string[];
|
|
@@ -81,7 +81,7 @@ describe('Resource options parser tests', () => {
|
|
|
81
81
|
const option: ResourceSettings<TestConfig> = {
|
|
82
82
|
id: 'typeId',
|
|
83
83
|
schema,
|
|
84
|
-
|
|
84
|
+
importAndDestroy: {
|
|
85
85
|
requiredParameters: ['import-error']
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -104,7 +104,7 @@ describe('Resource options parser tests', () => {
|
|
|
104
104
|
const option: ResourceSettings<TestConfig> = {
|
|
105
105
|
id: 'typeId',
|
|
106
106
|
schema,
|
|
107
|
-
|
|
107
|
+
importAndDestroy: {
|
|
108
108
|
refreshKeys: ['import-error']
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -127,7 +127,7 @@ describe('Resource options parser tests', () => {
|
|
|
127
127
|
const option: ResourceSettings<TestConfig> = {
|
|
128
128
|
id: 'typeId',
|
|
129
129
|
schema,
|
|
130
|
-
|
|
130
|
+
importAndDestroy: {
|
|
131
131
|
refreshKeys: ['remote'],
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -150,7 +150,7 @@ describe('Resource options parser tests', () => {
|
|
|
150
150
|
const option: ResourceSettings<TestConfig> = {
|
|
151
151
|
id: 'typeId',
|
|
152
152
|
schema,
|
|
153
|
-
|
|
153
|
+
importAndDestroy: {
|
|
154
154
|
defaultRefreshValues: {
|
|
155
155
|
repository: 'abc'
|
|
156
156
|
}
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
ArrayParameterSetting,
|
|
7
7
|
DefaultParameterSetting,
|
|
8
8
|
ParameterSetting,
|
|
9
|
+
resolveElementEqualsFn,
|
|
9
10
|
resolveEqualsFn,
|
|
10
|
-
resolveFnFromEqualsFnOrString,
|
|
11
11
|
resolveParameterTransformFn,
|
|
12
12
|
ResourceSettings,
|
|
13
13
|
StatefulParameterSetting
|
|
@@ -35,8 +35,12 @@ export type ParsedParameterSetting =
|
|
|
35
35
|
export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
|
|
36
36
|
private cache = new Map<string, unknown>();
|
|
37
37
|
id!: string;
|
|
38
|
-
schema?:
|
|
39
|
-
allowMultiple?: {
|
|
38
|
+
schema?: Partial<JSONSchemaType<T | any>>;
|
|
39
|
+
allowMultiple?: {
|
|
40
|
+
matcher?: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
|
|
41
|
+
requiredParameters?: string[]
|
|
42
|
+
} | boolean;
|
|
43
|
+
|
|
40
44
|
removeStatefulParametersBeforeDestroy?: boolean | undefined;
|
|
41
45
|
dependencies?: string[] | undefined;
|
|
42
46
|
inputTransformation?: ((desired: Partial<T>) => unknown) | undefined;
|
|
@@ -95,8 +99,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
95
99
|
if (v.type === 'array') {
|
|
96
100
|
const parsed = {
|
|
97
101
|
...v,
|
|
98
|
-
isElementEqual:
|
|
99
|
-
?? ((a: unknown, b: unknown) => a === b),
|
|
102
|
+
isElementEqual: resolveElementEqualsFn(v as ArrayParameterSetting)
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
return [k, parsed as ParsedArrayParameterSetting];
|
|
@@ -142,7 +145,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
142
145
|
return Object.fromEntries(
|
|
143
146
|
Object.entries(this.settings.parameterSettings)
|
|
144
147
|
.filter(([_, v]) => resolveParameterTransformFn(v!) !== undefined)
|
|
145
|
-
.map(([k, v]) => [k, resolveParameterTransformFn(v!)] as const)
|
|
148
|
+
.map(([k, v]) => [k, resolveParameterTransformFn(v!)!.to] as const)
|
|
146
149
|
) as Record<keyof T, (a: unknown) => unknown>;
|
|
147
150
|
});
|
|
148
151
|
}
|
|
@@ -186,7 +189,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
const schema = this.settings.schema as JSONSchemaType<any>;
|
|
189
|
-
if (!this.settings.
|
|
192
|
+
if (!this.settings.importAndDestroy && (schema?.oneOf
|
|
190
193
|
&& Array.isArray(schema.oneOf)
|
|
191
194
|
&& schema.oneOf.some((s) => s.required)
|
|
192
195
|
)
|
|
@@ -214,8 +217,8 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
214
217
|
)
|
|
215
218
|
}
|
|
216
219
|
|
|
217
|
-
if (this.settings.
|
|
218
|
-
const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.
|
|
220
|
+
if (this.settings.importAndDestroy) {
|
|
221
|
+
const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.importAndDestroy;
|
|
219
222
|
|
|
220
223
|
const requiredParametersNotInSchema = requiredParameters
|
|
221
224
|
?.filter(
|
|
@@ -158,6 +158,33 @@ export class ResourceController<T extends StringIndexedObject> {
|
|
|
158
158
|
})
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
async planDestroy(
|
|
162
|
+
core: ResourceConfig,
|
|
163
|
+
parameters: Partial<T>
|
|
164
|
+
): Promise<Plan<T>> {
|
|
165
|
+
this.addDefaultValues(parameters);
|
|
166
|
+
await this.applyTransformParameters(parameters);
|
|
167
|
+
|
|
168
|
+
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
169
|
+
const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
|
|
170
|
+
? {
|
|
171
|
+
...Object.fromEntries(
|
|
172
|
+
this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])
|
|
173
|
+
),
|
|
174
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
175
|
+
...parameters,
|
|
176
|
+
}
|
|
177
|
+
: {
|
|
178
|
+
...Object.fromEntries(
|
|
179
|
+
this.getAllParameterKeys().map((k) => [k, null])
|
|
180
|
+
),
|
|
181
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
182
|
+
...parameters,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return this.plan(core, null, parametersToRefresh, true);
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
async apply(plan: Plan<T>): Promise<void> {
|
|
162
189
|
if (plan.getResourceType() !== this.typeId) {
|
|
163
190
|
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
@@ -191,19 +218,19 @@ export class ResourceController<T extends StringIndexedObject> {
|
|
|
191
218
|
await this.applyTransformParameters(parameters);
|
|
192
219
|
|
|
193
220
|
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
194
|
-
const parametersToRefresh = this.settings.
|
|
221
|
+
const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys
|
|
195
222
|
? {
|
|
196
223
|
...Object.fromEntries(
|
|
197
|
-
this.settings.
|
|
224
|
+
this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])
|
|
198
225
|
),
|
|
199
|
-
...this.settings.
|
|
226
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
200
227
|
...parameters,
|
|
201
228
|
}
|
|
202
229
|
: {
|
|
203
230
|
...Object.fromEntries(
|
|
204
231
|
this.getAllParameterKeys().map((k) => [k, null])
|
|
205
232
|
),
|
|
206
|
-
...this.settings.
|
|
233
|
+
...this.settings.importAndDestroy?.defaultRefreshValues,
|
|
207
234
|
...parameters,
|
|
208
235
|
};
|
|
209
236
|
|
|
@@ -637,7 +637,7 @@ describe('Resource parameter tests', () => {
|
|
|
637
637
|
getSettings(): ResourceSettings<TestConfig> {
|
|
638
638
|
return {
|
|
639
639
|
id: 'resourceType',
|
|
640
|
-
|
|
640
|
+
importAndDestroy: {
|
|
641
641
|
requiredParameters: [
|
|
642
642
|
'propA',
|
|
643
643
|
'propB',
|
|
@@ -723,7 +723,7 @@ describe('Resource parameter tests', () => {
|
|
|
723
723
|
getSettings(): ResourceSettings<TestConfig> {
|
|
724
724
|
return {
|
|
725
725
|
id: 'resourceType',
|
|
726
|
-
|
|
726
|
+
importAndDestroy: {
|
|
727
727
|
requiredParameters: ['propA'],
|
|
728
728
|
refreshKeys: ['propB', 'propA'],
|
|
729
729
|
defaultRefreshValues: {
|
|
@@ -852,16 +852,25 @@ describe('Resource parameter tests', () => {
|
|
|
852
852
|
parameterSettings: {
|
|
853
853
|
propD: {
|
|
854
854
|
type: 'array',
|
|
855
|
-
inputTransformation:
|
|
855
|
+
inputTransformation: {
|
|
856
|
+
to: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
|
|
857
|
+
Object.entries(h)
|
|
858
|
+
.map(([k, v]) => [
|
|
859
|
+
k,
|
|
860
|
+
typeof v === 'boolean'
|
|
861
|
+
? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
|
|
862
|
+
: v,
|
|
863
|
+
])
|
|
864
|
+
)
|
|
865
|
+
),
|
|
866
|
+
from: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
|
|
856
867
|
Object.entries(h)
|
|
857
868
|
.map(([k, v]) => [
|
|
858
869
|
k,
|
|
859
|
-
|
|
860
|
-
? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
|
|
861
|
-
: v,
|
|
870
|
+
v === 'yes',
|
|
862
871
|
])
|
|
863
|
-
)
|
|
864
|
-
|
|
872
|
+
))
|
|
873
|
+
}
|
|
865
874
|
}
|
|
866
875
|
}
|
|
867
876
|
}
|
|
@@ -909,16 +918,25 @@ describe('Resource parameter tests', () => {
|
|
|
909
918
|
getSettings(): any {
|
|
910
919
|
return {
|
|
911
920
|
type: 'array',
|
|
912
|
-
inputTransformation:
|
|
921
|
+
inputTransformation: {
|
|
922
|
+
to: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
|
|
923
|
+
Object.entries(h)
|
|
924
|
+
.map(([k, v]) => [
|
|
925
|
+
k,
|
|
926
|
+
typeof v === 'boolean'
|
|
927
|
+
? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
|
|
928
|
+
: v,
|
|
929
|
+
])
|
|
930
|
+
)
|
|
931
|
+
),
|
|
932
|
+
from: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
|
|
913
933
|
Object.entries(h)
|
|
914
934
|
.map(([k, v]) => [
|
|
915
935
|
k,
|
|
916
|
-
|
|
917
|
-
? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
|
|
918
|
-
: v,
|
|
936
|
+
v === 'yes',
|
|
919
937
|
])
|
|
920
|
-
)
|
|
921
|
-
|
|
938
|
+
))
|
|
939
|
+
}
|
|
922
940
|
}
|
|
923
941
|
}
|
|
924
942
|
|
|
@@ -969,4 +987,59 @@ describe('Resource parameter tests', () => {
|
|
|
969
987
|
);
|
|
970
988
|
|
|
971
989
|
})
|
|
990
|
+
|
|
991
|
+
it('Supports equality check for itemType', async () => {
|
|
992
|
+
const resource = new class extends TestResource {
|
|
993
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
994
|
+
return {
|
|
995
|
+
id: 'resourceType',
|
|
996
|
+
parameterSettings: {
|
|
997
|
+
propA: { type: 'array', itemType: 'version' }
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
1003
|
+
return {
|
|
1004
|
+
propA: ['10.0.0']
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const controller = new ResourceController(resource);
|
|
1010
|
+
|
|
1011
|
+
const result = await controller.plan({ type: 'resourceType' }, { propA: ['10.0'] }, null, false);
|
|
1012
|
+
expect(result.changeSet).toMatchObject({
|
|
1013
|
+
operation: ResourceOperation.NOOP,
|
|
1014
|
+
})
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
it('Supports transformations for itemType', async () => {
|
|
1018
|
+
const home = os.homedir()
|
|
1019
|
+
const testPath = path.join(home, 'test/folder');
|
|
1020
|
+
|
|
1021
|
+
const resource = new class extends TestResource {
|
|
1022
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
1023
|
+
return {
|
|
1024
|
+
id: 'resourceType',
|
|
1025
|
+
parameterSettings: {
|
|
1026
|
+
propA: { type: 'array', itemType: 'directory' }
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
1032
|
+
return {
|
|
1033
|
+
propA: [testPath]
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
const controller = new ResourceController(resource);
|
|
1039
|
+
|
|
1040
|
+
const result = await controller.plan({ type: 'resourceType' }, { propA: ['~/test/folder'] }, null, false);
|
|
1041
|
+
expect(result.changeSet).toMatchObject({
|
|
1042
|
+
operation: ResourceOperation.NOOP,
|
|
1043
|
+
})
|
|
1044
|
+
})
|
|
972
1045
|
})
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { JSONSchemaType } from 'ajv';
|
|
1
2
|
import { StringIndexedObject } from 'codify-schemas';
|
|
2
3
|
import isObjectsEqual from 'lodash.isequal'
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
6
|
-
import { areArraysEqual, untildify } from '../utils/utils.js';
|
|
7
|
+
import { areArraysEqual, tildify, untildify } from '../utils/utils.js';
|
|
8
|
+
|
|
9
|
+
export interface InputTransformation {
|
|
10
|
+
to: (input: any) => Promise<any> | any;
|
|
11
|
+
from: (current: any) => Promise<any> | any;
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* The configuration and settings for a resource.
|
|
@@ -18,7 +24,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
18
24
|
/**
|
|
19
25
|
* Schema to validate user configs with. Must be in the format JSON Schema draft07
|
|
20
26
|
*/
|
|
21
|
-
schema?:
|
|
27
|
+
schema?: Partial<JSONSchemaType<T | any>>;
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Allow multiple of the same resource to unique. Set truthy if
|
|
@@ -27,6 +33,17 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
27
33
|
*/
|
|
28
34
|
allowMultiple?: {
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A set of parameters that uniquely identifies a resource. The value of these parameters is used to determine which
|
|
38
|
+
* resource is which when multiple can exist at the same time. Defaults to the required parameters inside the json
|
|
39
|
+
* schema.
|
|
40
|
+
*
|
|
41
|
+
* For example:
|
|
42
|
+
* If paramA is required, then if resource1.paramA === resource2.paramA then are the same resource.
|
|
43
|
+
* If resource1.paramA !== resource1.paramA, then they are different.
|
|
44
|
+
*/
|
|
45
|
+
requiredParameters?: string[]
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* If multiple copies are allowed then a matcher must be defined to match the desired
|
|
32
49
|
* config with one of the resources currently existing on the system. Return null if there is no match.
|
|
@@ -36,8 +53,8 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
36
53
|
*
|
|
37
54
|
* @return The matched resource.
|
|
38
55
|
*/
|
|
39
|
-
matcher
|
|
40
|
-
}
|
|
56
|
+
matcher?: (desired: Partial<T>, current: Partial<T>[],) => Partial<T>
|
|
57
|
+
} | boolean
|
|
41
58
|
|
|
42
59
|
/**
|
|
43
60
|
* If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
|
|
@@ -67,9 +84,9 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
67
84
|
inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
|
|
68
85
|
|
|
69
86
|
/**
|
|
70
|
-
* Customize the import behavior of the resource. By default, <code>codify import</code> will call
|
|
71
|
-
* every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
|
|
72
|
-
* in the schema and will prompt the user for these values before performing the import.
|
|
87
|
+
* Customize the import and destory behavior of the resource. By default, <code>codify import</code> and <code>codify destroy</code> will call
|
|
88
|
+
* `refresh()` with every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
|
|
89
|
+
* in the schema and will prompt the user for these values before performing the import or destroy.
|
|
73
90
|
*
|
|
74
91
|
* <b>Example:</b><br>
|
|
75
92
|
* Resource `alias` with parameters
|
|
@@ -85,7 +102,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
85
102
|
* { type: 'alias', alias: 'user-input', value: 'git push' }
|
|
86
103
|
* ```
|
|
87
104
|
*/
|
|
88
|
-
|
|
105
|
+
importAndDestroy?: {
|
|
89
106
|
|
|
90
107
|
/**
|
|
91
108
|
* Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
|
|
@@ -96,7 +113,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
96
113
|
* the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
|
|
97
114
|
* chooses which alias the resource is managing).
|
|
98
115
|
*
|
|
99
|
-
* See {@link
|
|
116
|
+
* See {@link importAndDestroy} for more information on how importing works.
|
|
100
117
|
*/
|
|
101
118
|
requiredParameters?: Array<Partial<keyof T>>;
|
|
102
119
|
|
|
@@ -107,14 +124,14 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
107
124
|
* By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
|
|
108
125
|
* in value can be customized using {@link defaultRefreshValues}
|
|
109
126
|
*
|
|
110
|
-
* See {@link
|
|
127
|
+
* See {@link importAndDestroy} for more information on how importing works.
|
|
111
128
|
*/
|
|
112
129
|
refreshKeys?: Array<Partial<keyof T>>;
|
|
113
130
|
|
|
114
131
|
/**
|
|
115
132
|
* Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
|
|
116
133
|
*
|
|
117
|
-
* See {@link
|
|
134
|
+
* See {@link importAndDestroy} for more information on how importing works.
|
|
118
135
|
*/
|
|
119
136
|
defaultRefreshValues?: Partial<T>
|
|
120
137
|
}
|
|
@@ -166,12 +183,13 @@ export interface DefaultParameterSetting {
|
|
|
166
183
|
default?: unknown;
|
|
167
184
|
|
|
168
185
|
/**
|
|
169
|
-
* A transformation of the input value for this parameter.
|
|
170
|
-
*
|
|
186
|
+
* A transformation of the input value for this parameter. Two transformations need to be provided: to (from desired to
|
|
187
|
+
* the internal type), and from (from the internal type back to desired). All transformations need to be bi-directional
|
|
188
|
+
* to support imports properly
|
|
171
189
|
*
|
|
172
190
|
* @param input The original parameter value from the desired config.
|
|
173
191
|
*/
|
|
174
|
-
inputTransformation?:
|
|
192
|
+
inputTransformation?: InputTransformation;
|
|
175
193
|
|
|
176
194
|
/**
|
|
177
195
|
* Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
|
|
@@ -233,6 +251,12 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
233
251
|
* Defaults to true.
|
|
234
252
|
*/
|
|
235
253
|
filterInStatelessMode?: ((desired: any[], current: any[]) => any[]) | boolean,
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The type of the array item. See {@link ParameterSettingType} for the available options. This value
|
|
257
|
+
* is mainly used to determine the equality method when performing diffing.
|
|
258
|
+
*/
|
|
259
|
+
itemType?: ParameterSettingType,
|
|
236
260
|
}
|
|
237
261
|
|
|
238
262
|
/**
|
|
@@ -273,10 +297,7 @@ export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown,
|
|
|
273
297
|
const isEqual = resolveFnFromEqualsFnOrString(parameter.isEqual);
|
|
274
298
|
|
|
275
299
|
if (parameter.type === 'array') {
|
|
276
|
-
|
|
277
|
-
const isElementEqual = resolveFnFromEqualsFnOrString(arrayParameter.isElementEqual);
|
|
278
|
-
|
|
279
|
-
return isEqual ?? areArraysEqual.bind(areArraysEqual, isElementEqual)
|
|
300
|
+
return isEqual ?? areArraysEqual.bind(areArraysEqual, resolveElementEqualsFn(parameter as ArrayParameterSetting))
|
|
280
301
|
}
|
|
281
302
|
|
|
282
303
|
if (parameter.type === 'stateful') {
|
|
@@ -286,6 +307,21 @@ export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown,
|
|
|
286
307
|
return isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
|
|
287
308
|
}
|
|
288
309
|
|
|
310
|
+
export function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean {
|
|
311
|
+
if (parameter.isElementEqual) {
|
|
312
|
+
const elementEq = resolveFnFromEqualsFnOrString(parameter.isElementEqual);
|
|
313
|
+
if (elementEq) {
|
|
314
|
+
return elementEq;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (parameter.itemType && ParameterEqualsDefaults[parameter.itemType]) {
|
|
319
|
+
return ParameterEqualsDefaults[parameter.itemType]!
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (a, b) => a === b;
|
|
323
|
+
}
|
|
324
|
+
|
|
289
325
|
// This resolves the fn if it is a string.
|
|
290
326
|
// A string can be specified to use a default equals method
|
|
291
327
|
export function resolveFnFromEqualsFnOrString(
|
|
@@ -303,19 +339,47 @@ export function resolveFnFromEqualsFnOrString(
|
|
|
303
339
|
return fnOrString as ((a: unknown, b: unknown) => boolean) | undefined;
|
|
304
340
|
}
|
|
305
341
|
|
|
306
|
-
const ParameterTransformationDefaults: Partial<Record<ParameterSettingType,
|
|
307
|
-
'directory':
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return (sp.definition?.getSettings()?.inputTransformation)
|
|
311
|
-
? (sp.definition.getSettings().inputTransformation!(a))
|
|
312
|
-
: a;
|
|
342
|
+
const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, InputTransformation>> = {
|
|
343
|
+
'directory': {
|
|
344
|
+
to: (a: unknown) => path.resolve(untildify(String(a))),
|
|
345
|
+
from: (a: unknown) => tildify(String(a)),
|
|
313
346
|
},
|
|
314
|
-
'string':
|
|
347
|
+
'string': {
|
|
348
|
+
to: String,
|
|
349
|
+
from: String,
|
|
350
|
+
}
|
|
315
351
|
}
|
|
316
352
|
|
|
317
353
|
export function resolveParameterTransformFn(
|
|
318
354
|
parameter: ParameterSetting
|
|
319
|
-
):
|
|
355
|
+
): InputTransformation | undefined {
|
|
356
|
+
|
|
357
|
+
if (parameter.type === 'stateful' && !parameter.inputTransformation) {
|
|
358
|
+
const sp = (parameter as StatefulParameterSetting).definition.getSettings();
|
|
359
|
+
if (sp.inputTransformation) {
|
|
360
|
+
return (parameter as StatefulParameterSetting).definition?.getSettings()?.inputTransformation
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return sp.type ? ParameterTransformationDefaults[sp.type] : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (parameter.type === 'array'
|
|
367
|
+
&& (parameter as ArrayParameterSetting).itemType
|
|
368
|
+
&& ParameterTransformationDefaults[(parameter as ArrayParameterSetting).itemType!]
|
|
369
|
+
&& !parameter.inputTransformation
|
|
370
|
+
) {
|
|
371
|
+
const itemType = (parameter as ArrayParameterSetting).itemType!;
|
|
372
|
+
const itemTransformation = ParameterTransformationDefaults[itemType]!;
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
to(input: unknown[]) {
|
|
376
|
+
return input.map((i) => itemTransformation.to(i))
|
|
377
|
+
},
|
|
378
|
+
from(input: unknown[]) {
|
|
379
|
+
return input.map((i) => itemTransformation.from(i))
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
320
384
|
return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined;
|
|
321
385
|
}
|
|
@@ -5,8 +5,8 @@ import { ParsedArrayParameterSetting, ParsedParameterSetting, } from '../resourc
|
|
|
5
5
|
import {
|
|
6
6
|
ArrayParameterSetting,
|
|
7
7
|
ParameterSetting,
|
|
8
|
+
resolveElementEqualsFn,
|
|
8
9
|
resolveEqualsFn,
|
|
9
|
-
resolveFnFromEqualsFnOrString
|
|
10
10
|
} from '../resource/resource-settings.js';
|
|
11
11
|
import { ArrayStatefulParameter, StatefulParameter } from './stateful-parameter.js';
|
|
12
12
|
|
|
@@ -31,8 +31,7 @@ export class StatefulParameterController<T extends StringIndexedObject, V extend
|
|
|
31
31
|
this.parsedSettings = (this.isArrayStatefulParameter || this.settings.type === 'array') ? {
|
|
32
32
|
...this.settings,
|
|
33
33
|
isEqual: resolveEqualsFn(this.settings),
|
|
34
|
-
isElementEqual:
|
|
35
|
-
?? ((a: unknown, b: unknown) => a === b)
|
|
34
|
+
isElementEqual: resolveElementEqualsFn(this.settings as ArrayParameterSetting)
|
|
36
35
|
} as ParsedParameterSetting : {
|
|
37
36
|
...this.settings,
|
|
38
37
|
isEqual: resolveEqualsFn(this.settings),
|
package/src/utils/utils.ts
CHANGED
|
@@ -33,6 +33,10 @@ export function untildify(pathWithTilde: string) {
|
|
|
33
33
|
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function tildify(pathWithTilde: string) {
|
|
37
|
+
return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde;
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
export function areArraysEqual(
|
|
37
41
|
isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined,
|
|
38
42
|
desired: unknown,
|