codify-plugin-lib 1.0.99 → 1.0.101
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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plan/change-set.d.ts +2 -2
- package/dist/plan/plan.js +16 -8
- package/dist/resource/config-parser.d.ts +2 -2
- package/dist/resource/parsed-resource-settings.d.ts +17 -4
- package/dist/resource/parsed-resource-settings.js +25 -3
- package/dist/resource/resource-settings.d.ts +70 -6
- package/dist/resource/resource-settings.js +21 -5
- package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
- package/dist/stateful-parameter/stateful-parameter-controller.js +82 -0
- package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
- package/dist/stateful-parameter/stateful-parameter.js +43 -0
- package/dist/utils/utils.d.ts +1 -2
- package/dist/utils/utils.js +3 -2
- package/package.json +4 -2
- package/src/index.ts +1 -1
- package/src/plan/change-set.ts +5 -5
- package/src/plan/plan.test.ts +78 -0
- package/src/plan/plan.ts +24 -11
- package/src/resource/config-parser.ts +3 -3
- package/src/resource/parsed-resource-settings.ts +56 -9
- package/src/resource/resource-settings.test.ts +101 -0
- package/src/resource/resource-settings.ts +99 -9
- package/src/{resource/stateful-parameter.test.ts → stateful-parameter/stateful-parameter-controller.test.ts} +90 -13
- package/src/stateful-parameter/stateful-parameter-controller.ts +112 -0
- package/src/{resource → stateful-parameter}/stateful-parameter.ts +9 -67
- package/src/utils/test-utils.test.ts +1 -1
- package/src/utils/utils.ts +9 -4
package/dist/utils/utils.js
CHANGED
|
@@ -72,7 +72,7 @@ const homeDirectory = os.homedir();
|
|
|
72
72
|
export function untildify(pathWithTilde) {
|
|
73
73
|
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
74
74
|
}
|
|
75
|
-
export function areArraysEqual(
|
|
75
|
+
export function areArraysEqual(isElementEqual, desired, current) {
|
|
76
76
|
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
77
77
|
throw new Error(`A non-array value:
|
|
78
78
|
|
|
@@ -91,7 +91,8 @@ Was provided even though type array was specified.
|
|
|
91
91
|
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
92
92
|
// an isElementEqual method. Time: O(n^2)
|
|
93
93
|
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
94
|
-
const idx = currentCopy.findIndex((e2) => (
|
|
94
|
+
const idx = currentCopy.findIndex((e2) => (isElementEqual
|
|
95
|
+
?? ((a, b) => a === b))(desiredCopy[counter], e2));
|
|
95
96
|
if (idx === -1) {
|
|
96
97
|
return false;
|
|
97
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.101",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"ajv-formats": "^2.1.1",
|
|
17
17
|
"codify-schemas": "1.0.52",
|
|
18
18
|
"@npmcli/promise-spawn": "^7.0.1",
|
|
19
|
-
"uuid": "^10.0.0"
|
|
19
|
+
"uuid": "^10.0.0",
|
|
20
|
+
"lodash.isequal": "^4.5.0"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@oclif/prettier-config": "^0.2.1",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"@types/semver": "^7.5.4",
|
|
27
28
|
"@types/sinon": "^17.0.3",
|
|
28
29
|
"@types/uuid": "^10.0.0",
|
|
30
|
+
"@types/lodash.isequal": "^4.5.8",
|
|
29
31
|
"chai-as-promised": "^7.1.1",
|
|
30
32
|
"vitest": "^1.4.0",
|
|
31
33
|
"vitest-mock-extended": "^1.3.1",
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from './plugin/plugin.js'
|
|
|
9
9
|
export * from './resource/parsed-resource-settings.js';
|
|
10
10
|
export * from './resource/resource.js'
|
|
11
11
|
export * from './resource/resource-settings.js'
|
|
12
|
-
export * from './
|
|
12
|
+
export * from './stateful-parameter/stateful-parameter.js'
|
|
13
13
|
export * from './utils/utils.js'
|
|
14
14
|
|
|
15
15
|
export async function runPlugin(plugin: Plugin) {
|
package/src/plan/change-set.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ParsedParameterSetting } from '../resource/parsed-resource-settings.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A parameter change describes a parameter level change to a resource.
|
|
@@ -87,7 +87,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
87
87
|
static calculateModification<T extends StringIndexedObject>(
|
|
88
88
|
desired: Partial<T>,
|
|
89
89
|
current: Partial<T>,
|
|
90
|
-
parameterSettings: Partial<Record<keyof T,
|
|
90
|
+
parameterSettings: Partial<Record<keyof T, ParsedParameterSetting>> = {},
|
|
91
91
|
): ChangeSet<T> {
|
|
92
92
|
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
93
93
|
|
|
@@ -128,7 +128,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
128
128
|
private static calculateParameterChanges<T extends StringIndexedObject>(
|
|
129
129
|
desiredParameters: Partial<T>,
|
|
130
130
|
currentParameters: Partial<T>,
|
|
131
|
-
parameterOptions?: Partial<Record<keyof T,
|
|
131
|
+
parameterOptions?: Partial<Record<keyof T, ParsedParameterSetting>>,
|
|
132
132
|
): ParameterChange<T>[] {
|
|
133
133
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
134
134
|
|
|
@@ -204,8 +204,8 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
204
204
|
private static isSame(
|
|
205
205
|
desired: unknown,
|
|
206
206
|
current: unknown,
|
|
207
|
-
setting?:
|
|
207
|
+
setting?: ParsedParameterSetting,
|
|
208
208
|
): boolean {
|
|
209
|
-
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current)
|
|
209
|
+
return (setting?.isEqual ?? ((a: unknown, b: unknown) => a === b))(desired, current)
|
|
210
210
|
}
|
|
211
211
|
}
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -147,6 +147,84 @@ describe('Plan entity tests', () => {
|
|
|
147
147
|
operation: ResourceOperation.RECREATE
|
|
148
148
|
})
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
it('Filters array parameters in stateless mode (by default)', async () => {
|
|
152
|
+
const resource = new class extends TestResource {
|
|
153
|
+
getSettings(): ResourceSettings<any> {
|
|
154
|
+
return {
|
|
155
|
+
id: 'type',
|
|
156
|
+
parameterSettings: {
|
|
157
|
+
propZ: { type: 'array', isElementEqual: (a, b) => b.includes(a) }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
163
|
+
return {
|
|
164
|
+
propZ: [
|
|
165
|
+
'20.15.0',
|
|
166
|
+
'20.15.1'
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const controller = new ResourceController(resource);
|
|
173
|
+
const plan = await controller.plan({
|
|
174
|
+
propZ: ['20.15'],
|
|
175
|
+
} as any)
|
|
176
|
+
|
|
177
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('Doesn\'t filters array parameters if filtering is disabled', async () => {
|
|
181
|
+
const resource = new class extends TestResource {
|
|
182
|
+
getSettings(): ResourceSettings<any> {
|
|
183
|
+
return {
|
|
184
|
+
id: 'type',
|
|
185
|
+
parameterSettings: {
|
|
186
|
+
propZ: {
|
|
187
|
+
type: 'array',
|
|
188
|
+
canModify: true,
|
|
189
|
+
isElementEqual: (a, b) => b.includes(a),
|
|
190
|
+
filterInStatelessMode: false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
197
|
+
return {
|
|
198
|
+
propZ: [
|
|
199
|
+
'20.15.0',
|
|
200
|
+
'20.15.1'
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const controller = new ResourceController(resource);
|
|
207
|
+
const plan = await controller.plan({
|
|
208
|
+
propZ: ['20.15'],
|
|
209
|
+
} as any)
|
|
210
|
+
|
|
211
|
+
expect(plan.changeSet).toMatchObject({
|
|
212
|
+
operation: ResourceOperation.MODIFY,
|
|
213
|
+
parameterChanges: expect.arrayContaining([
|
|
214
|
+
expect.objectContaining({
|
|
215
|
+
name: 'propZ',
|
|
216
|
+
previousValue: expect.arrayContaining([
|
|
217
|
+
'20.15.0',
|
|
218
|
+
'20.15.1'
|
|
219
|
+
]),
|
|
220
|
+
newValue: expect.arrayContaining([
|
|
221
|
+
'20.15'
|
|
222
|
+
]),
|
|
223
|
+
operation: 'modify'
|
|
224
|
+
})
|
|
225
|
+
])
|
|
226
|
+
})
|
|
227
|
+
})
|
|
150
228
|
})
|
|
151
229
|
|
|
152
230
|
function createTestResource() {
|
package/src/plan/plan.ts
CHANGED
|
@@ -8,8 +8,12 @@ import {
|
|
|
8
8
|
} from 'codify-schemas';
|
|
9
9
|
import { v4 as uuidV4 } from 'uuid';
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
ParsedArrayParameterSetting,
|
|
13
|
+
ParsedResourceSettings,
|
|
14
|
+
ParsedStatefulParameterSetting
|
|
15
|
+
} from '../resource/parsed-resource-settings.js';
|
|
16
|
+
import { ArrayParameterSetting, ResourceSettings } from '../resource/resource-settings.js';
|
|
13
17
|
import { ChangeSet } from './change-set.js';
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -220,7 +224,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
220
224
|
// TODO: Add object handling here in addition to arrays in the future
|
|
221
225
|
const arrayStatefulParameters = Object.fromEntries(
|
|
222
226
|
Object.entries(filteredCurrent)
|
|
223
|
-
.filter(([k, v]) =>
|
|
227
|
+
.filter(([k, v]) => isArrayParameterWithFiltering(k, v))
|
|
224
228
|
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
|
|
225
229
|
)
|
|
226
230
|
|
|
@@ -247,19 +251,28 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
247
251
|
) as Partial<T>;
|
|
248
252
|
}
|
|
249
253
|
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
+
function isArrayParameterWithFiltering(k: string, v: T[keyof T]): boolean {
|
|
255
|
+
if (settings.parameterSettings?.[k]?.type === 'stateful') {
|
|
256
|
+
const statefulSetting = settings.parameterSettings[k] as ParsedStatefulParameterSetting;
|
|
257
|
+
return statefulSetting.nestedSettings.type === 'array' &&
|
|
258
|
+
((statefulSetting.nestedSettings as ArrayParameterSetting).filterInStatelessMode ?? true)
|
|
259
|
+
&& Array.isArray(v);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return settings.parameterSettings?.[k]?.type === 'array'
|
|
263
|
+
&& ((settings.parameterSettings?.[k] as ArrayParameterSetting).filterInStatelessMode ?? true)
|
|
264
|
+
&& Array.isArray(v);
|
|
254
265
|
}
|
|
255
266
|
|
|
256
267
|
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
257
268
|
function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
|
|
258
269
|
const desiredArray = desired![k] as unknown[];
|
|
259
|
-
const matcher =
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
const matcher = settings.parameterSettings![k]!.type === 'stateful'
|
|
271
|
+
? ((settings.parameterSettings![k] as ParsedStatefulParameterSetting)
|
|
272
|
+
.nestedSettings as ParsedArrayParameterSetting)
|
|
273
|
+
.isElementEqual
|
|
274
|
+
: (settings.parameterSettings![k] as ParsedArrayParameterSetting)
|
|
275
|
+
.isElementEqual
|
|
263
276
|
|
|
264
277
|
const desiredCopy = [...desiredArray];
|
|
265
278
|
const currentCopy = [...v];
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
|
|
3
|
+
import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
|
|
3
4
|
import { splitUserConfig } from '../utils/utils.js';
|
|
4
|
-
import { StatefulParameter } from './stateful-parameter.js';
|
|
5
5
|
|
|
6
6
|
export class ConfigParser<T extends StringIndexedObject> {
|
|
7
7
|
private readonly desiredConfig: Partial<T> & ResourceConfig | null;
|
|
8
8
|
private readonly stateConfig: Partial<T> & ResourceConfig | null;
|
|
9
|
-
private statefulParametersMap: Map<keyof T,
|
|
9
|
+
private statefulParametersMap: Map<keyof T, StatefulParameterController<T, T[keyof T]>>;
|
|
10
10
|
|
|
11
11
|
constructor(
|
|
12
12
|
desiredConfig: Partial<T> & ResourceConfig | null,
|
|
13
13
|
stateConfig: Partial<T> & ResourceConfig | null,
|
|
14
|
-
statefulParameters: Map<keyof T,
|
|
14
|
+
statefulParameters: Map<keyof T, StatefulParameterController<T, T[keyof T]>>,
|
|
15
15
|
) {
|
|
16
16
|
this.desiredConfig = desiredConfig;
|
|
17
17
|
this.stateConfig = stateConfig
|
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import { JSONSchemaType } from 'ajv';
|
|
2
2
|
import { StringIndexedObject } from 'codify-schemas';
|
|
3
3
|
|
|
4
|
+
import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
|
|
4
5
|
import {
|
|
6
|
+
ArrayParameterSetting,
|
|
7
|
+
DefaultParameterSetting,
|
|
5
8
|
ParameterSetting,
|
|
6
9
|
resolveEqualsFn,
|
|
10
|
+
resolveFnFromEqualsFnOrString,
|
|
7
11
|
resolveParameterTransformFn,
|
|
8
12
|
ResourceSettings,
|
|
9
13
|
StatefulParameterSetting
|
|
10
14
|
} from './resource-settings.js';
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
export interface ParsedStatefulParameterSetting extends DefaultParameterSetting {
|
|
17
|
+
type: 'stateful',
|
|
18
|
+
controller: StatefulParameterController<any, unknown>
|
|
19
|
+
order?: number,
|
|
20
|
+
nestedSettings: ParsedParameterSetting;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ParsedArrayParameterSetting = {
|
|
24
|
+
isElementEqual: (a: unknown, b: unknown) => boolean;
|
|
25
|
+
isEqual: (a: unknown, b: unknown) => boolean;
|
|
26
|
+
} & ArrayParameterSetting
|
|
27
|
+
|
|
28
|
+
export type ParsedParameterSetting =
|
|
29
|
+
{
|
|
30
|
+
isEqual: (desired: unknown, current: unknown) => boolean;
|
|
31
|
+
} & (DefaultParameterSetting
|
|
32
|
+
| ParsedArrayParameterSetting
|
|
33
|
+
| ParsedStatefulParameterSetting)
|
|
12
34
|
|
|
13
35
|
export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
|
|
14
36
|
private cache = new Map<string, unknown>();
|
|
@@ -36,24 +58,49 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
36
58
|
return this.id;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
|
-
get statefulParameters(): Map<keyof T,
|
|
61
|
+
get statefulParameters(): Map<keyof T, StatefulParameterController<T, T[keyof T]>> {
|
|
40
62
|
return this.getFromCacheOrCreate('statefulParameters', () => {
|
|
41
63
|
|
|
42
64
|
const statefulParameters = Object.entries(this.settings.parameterSettings ?? {})
|
|
43
65
|
.filter(([, p]) => p?.type === 'stateful')
|
|
44
|
-
.map(([k, v]) => [
|
|
66
|
+
.map(([k, v]) => [
|
|
67
|
+
k,
|
|
68
|
+
new StatefulParameterController((v as StatefulParameterSetting).definition)
|
|
69
|
+
] as const)
|
|
45
70
|
|
|
46
|
-
return new Map(statefulParameters) as Map<keyof T,
|
|
71
|
+
return new Map(statefulParameters) as Map<keyof T, StatefulParameterController<T, T[keyof T]>>;
|
|
47
72
|
})
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
get parameterSettings(): Record<keyof T,
|
|
75
|
+
get parameterSettings(): Record<keyof T, ParsedParameterSetting> {
|
|
51
76
|
return this.getFromCacheOrCreate('parameterSetting', () => {
|
|
52
77
|
|
|
53
78
|
const settings = Object.entries(this.settings.parameterSettings ?? {})
|
|
54
79
|
.map(([k, v]) => [k, v!] as const)
|
|
55
80
|
.map(([k, v]) => {
|
|
56
|
-
v.isEqual = resolveEqualsFn(v
|
|
81
|
+
v.isEqual = resolveEqualsFn(v);
|
|
82
|
+
|
|
83
|
+
if (v.type === 'stateful') {
|
|
84
|
+
const spController = this.statefulParameters.get(k);
|
|
85
|
+
const parsed = {
|
|
86
|
+
...v,
|
|
87
|
+
controller: spController,
|
|
88
|
+
nestedSettings: spController?.parsedSettings,
|
|
89
|
+
definition: undefined,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return [k, parsed as ParsedStatefulParameterSetting];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (v.type === 'array') {
|
|
96
|
+
const parsed = {
|
|
97
|
+
...v,
|
|
98
|
+
isElementEqual: resolveFnFromEqualsFnOrString((v as ArrayParameterSetting).isElementEqual)
|
|
99
|
+
?? ((a: unknown, b: unknown) => a === b),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [k, parsed as ParsedArrayParameterSetting];
|
|
103
|
+
}
|
|
57
104
|
|
|
58
105
|
return [k, v];
|
|
59
106
|
})
|
|
@@ -171,9 +218,9 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
171
218
|
const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.import;
|
|
172
219
|
|
|
173
220
|
const requiredParametersNotInSchema = requiredParameters
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
221
|
+
?.filter(
|
|
222
|
+
(p) => schema && !(schema.properties[p])
|
|
223
|
+
)
|
|
177
224
|
if (schema && requiredParametersNotInSchema && requiredParametersNotInSchema.length > 0) {
|
|
178
225
|
throw new Error(`The following properties were declared in settings.import.requiredParameters but were not found in the schema:
|
|
179
226
|
${JSON.stringify(requiredParametersNotInSchema, null, 2)}`)
|
|
@@ -629,4 +629,105 @@ describe('Resource parameter tests', () => {
|
|
|
629
629
|
}
|
|
630
630
|
};
|
|
631
631
|
})
|
|
632
|
+
|
|
633
|
+
it('Accepts a string isEqual method which selects from one of the defaults', async () => {
|
|
634
|
+
const resource = new class extends TestResource {
|
|
635
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
636
|
+
return {
|
|
637
|
+
id: 'resourceType',
|
|
638
|
+
parameterSettings: {
|
|
639
|
+
propA: { type: 'string', isEqual: 'version' }
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
645
|
+
return {
|
|
646
|
+
propA: '10.0.0'
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const controller = new ResourceController(resource);
|
|
652
|
+
|
|
653
|
+
const result = await controller.plan({ type: 'resourceType', propA: '10.0' });
|
|
654
|
+
expect(result.changeSet).toMatchObject({
|
|
655
|
+
operation: ResourceOperation.NOOP,
|
|
656
|
+
})
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('Object equals method (works when equal)', async () => {
|
|
660
|
+
const resource = new class extends TestResource {
|
|
661
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
662
|
+
return {
|
|
663
|
+
id: 'resourceType',
|
|
664
|
+
parameterSettings: {
|
|
665
|
+
propD: { type: 'object' }
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
671
|
+
return {
|
|
672
|
+
propD: {
|
|
673
|
+
testA: 'a',
|
|
674
|
+
testB: 'b',
|
|
675
|
+
testC: 10,
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const controller = new ResourceController(resource);
|
|
682
|
+
|
|
683
|
+
const result = await controller.plan({
|
|
684
|
+
type: 'resourceType',
|
|
685
|
+
propD: {
|
|
686
|
+
testC: 10,
|
|
687
|
+
testA: 'a',
|
|
688
|
+
testB: 'b',
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
expect(result.changeSet).toMatchObject({
|
|
693
|
+
operation: ResourceOperation.NOOP,
|
|
694
|
+
})
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('Object equals method (works when not equal)', async () => {
|
|
698
|
+
const resource = new class extends TestResource {
|
|
699
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
700
|
+
return {
|
|
701
|
+
id: 'resourceType',
|
|
702
|
+
parameterSettings: {
|
|
703
|
+
propD: { type: 'object' }
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
|
|
709
|
+
return {
|
|
710
|
+
propD: {
|
|
711
|
+
testA: 'a',
|
|
712
|
+
testB: 'b',
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const controller = new ResourceController(resource);
|
|
719
|
+
|
|
720
|
+
const result = await controller.plan({
|
|
721
|
+
type: 'resourceType',
|
|
722
|
+
propD: {
|
|
723
|
+
testC: 10,
|
|
724
|
+
testA: 'a',
|
|
725
|
+
testB: 'b',
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(result.changeSet).toMatchObject({
|
|
730
|
+
operation: ResourceOperation.RECREATE,
|
|
731
|
+
})
|
|
732
|
+
});
|
|
632
733
|
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import isObjectsEqual from 'lodash.isequal'
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
5
|
+
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
4
6
|
import { areArraysEqual, untildify } from '../utils/utils.js';
|
|
5
|
-
import { StatefulParameter } from './stateful-parameter.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The configuration and settings for a resource.
|
|
@@ -65,11 +66,56 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
65
66
|
*/
|
|
66
67
|
inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Customize the import behavior of the resource. By default, <code>codify import</code> will call `refresh()` with
|
|
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.
|
|
73
|
+
*
|
|
74
|
+
* <b>Example:</b><br>
|
|
75
|
+
* Resource `alias` with parameters
|
|
76
|
+
*
|
|
77
|
+
* ```
|
|
78
|
+
* { alias <b>(*required)</b>: string; value: string; }
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* When the user calls `codify import alias`, they will first be prompted to enter the value for `alias`. Refresh
|
|
82
|
+
* is then called with `refresh({ alias: 'user-input', value: null })`. The result returned to the user will then be:
|
|
83
|
+
*
|
|
84
|
+
* ```
|
|
85
|
+
* { type: 'alias', alias: 'user-input', value: 'git push' }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
68
88
|
import?: {
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
|
|
92
|
+
* from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in
|
|
93
|
+
* the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`).
|
|
94
|
+
* <br>
|
|
95
|
+
* The user will be prompted for the required parameters before the import starts. This is done because for most resources
|
|
96
|
+
* the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
|
|
97
|
+
* chooses which alias the resource is managing).
|
|
98
|
+
*
|
|
99
|
+
* See {@link import} for more information on how importing works.
|
|
100
|
+
*/
|
|
69
101
|
requiredParameters?: Array<Partial<keyof T>>;
|
|
70
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Customize which keys will be refreshed in the import. Typically, `refresh()` statements only refresh
|
|
105
|
+
* the parameters provided as the input. Use `refreshKeys` to control which parameter keys are passed in.
|
|
106
|
+
* <br>
|
|
107
|
+
* By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
|
|
108
|
+
* in value can be customized using {@link defaultRefreshValues}
|
|
109
|
+
*
|
|
110
|
+
* See {@link import} for more information on how importing works.
|
|
111
|
+
*/
|
|
71
112
|
refreshKeys?: Array<Partial<keyof T>>;
|
|
72
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
|
|
116
|
+
*
|
|
117
|
+
* See {@link import} for more information on how importing works.
|
|
118
|
+
*/
|
|
73
119
|
defaultRefreshValues?: Partial<T>
|
|
74
120
|
}
|
|
75
121
|
}
|
|
@@ -85,6 +131,7 @@ export type ParameterSettingType =
|
|
|
85
131
|
| 'boolean'
|
|
86
132
|
| 'directory'
|
|
87
133
|
| 'number'
|
|
134
|
+
| 'object'
|
|
88
135
|
| 'setting'
|
|
89
136
|
| 'stateful'
|
|
90
137
|
| 'string'
|
|
@@ -136,7 +183,7 @@ export interface DefaultParameterSetting {
|
|
|
136
183
|
*
|
|
137
184
|
* @return Return true if equal
|
|
138
185
|
*/
|
|
139
|
-
isEqual?: (desired: any, current: any) => boolean;
|
|
186
|
+
isEqual?: ((desired: any, current: any) => boolean) | ParameterSettingType;
|
|
140
187
|
|
|
141
188
|
/**
|
|
142
189
|
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
@@ -165,7 +212,27 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
165
212
|
*
|
|
166
213
|
* @return Return true if desired is equivalent to current.
|
|
167
214
|
*/
|
|
168
|
-
isElementEqual?: (desired: any, current: any) => boolean
|
|
215
|
+
isElementEqual?: ((desired: any, current: any) => boolean) | ParameterSettingType;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Filter the contents of the refreshed array by the desired. This way items currently on the system but not
|
|
219
|
+
* in desired don't show up in the plan.
|
|
220
|
+
*
|
|
221
|
+
* <b>For example, for the nvm resource:</b>
|
|
222
|
+
* <ul>
|
|
223
|
+
* <li>Desired (20.18.0, 18.9.0, 16.3.1)</li>
|
|
224
|
+
* <li>Current (20.18.0, 22.1.3, 12.1.0)</li>
|
|
225
|
+
* </ul>
|
|
226
|
+
*
|
|
227
|
+
* Without filtering the plan will be:
|
|
228
|
+
* (~20.18.0, +18.9.0, +16.3.1, -22.1.3, -12.1.0)<br>
|
|
229
|
+
* With filtering the plan is: (~20.18.0, +18.9.0, +16.3.1)
|
|
230
|
+
*
|
|
231
|
+
* As you can see, filtering prevents items currently installed on the system from being removed.
|
|
232
|
+
*
|
|
233
|
+
* Defaults to true.
|
|
234
|
+
*/
|
|
235
|
+
filterInStatelessMode?: boolean,
|
|
169
236
|
}
|
|
170
237
|
|
|
171
238
|
/**
|
|
@@ -184,7 +251,7 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
|
184
251
|
* as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
|
|
185
252
|
* modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
|
|
186
253
|
*/
|
|
187
|
-
definition: StatefulParameter<any, unknown>,
|
|
254
|
+
definition: ArrayStatefulParameter<any, unknown> | StatefulParameter<any, unknown>,
|
|
188
255
|
|
|
189
256
|
/**
|
|
190
257
|
* The order multiple stateful parameters should be applied in. The order is applied in ascending order (1, 2, 3...).
|
|
@@ -198,19 +265,42 @@ const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown,
|
|
|
198
265
|
'number': (a: unknown, b: unknown) => Number(a) === Number(b),
|
|
199
266
|
'string': (a: unknown, b: unknown) => String(a) === String(b),
|
|
200
267
|
'version': (desired: unknown, current: unknown) => String(current).includes(String(desired)),
|
|
201
|
-
'setting': (
|
|
268
|
+
'setting': () => true,
|
|
269
|
+
'object': isObjectsEqual,
|
|
202
270
|
}
|
|
203
271
|
|
|
204
|
-
export function resolveEqualsFn(parameter: ParameterSetting
|
|
272
|
+
export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, current: unknown) => boolean {
|
|
273
|
+
const isEqual = resolveFnFromEqualsFnOrString(parameter.isEqual);
|
|
274
|
+
|
|
205
275
|
if (parameter.type === 'array') {
|
|
206
|
-
|
|
276
|
+
const arrayParameter = parameter as ArrayParameterSetting;
|
|
277
|
+
const isElementEqual = resolveFnFromEqualsFnOrString(arrayParameter.isElementEqual);
|
|
278
|
+
|
|
279
|
+
return isEqual ?? areArraysEqual.bind(areArraysEqual, isElementEqual)
|
|
207
280
|
}
|
|
208
281
|
|
|
209
282
|
if (parameter.type === 'stateful') {
|
|
210
|
-
return resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings()
|
|
283
|
+
return resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings())
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// This resolves the fn if it is a string.
|
|
290
|
+
// A string can be specified to use a default equals method
|
|
291
|
+
export function resolveFnFromEqualsFnOrString(
|
|
292
|
+
fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined,
|
|
293
|
+
): ((a: unknown, b: unknown) => boolean) | undefined {
|
|
294
|
+
|
|
295
|
+
if (fnOrString && typeof fnOrString === 'string') {
|
|
296
|
+
if (!ParameterEqualsDefaults[fnOrString]) {
|
|
297
|
+
throw new Error(`isEqual of type ${fnOrString} was not found`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return ParameterEqualsDefaults[fnOrString]!
|
|
211
301
|
}
|
|
212
302
|
|
|
213
|
-
return
|
|
303
|
+
return fnOrString as ((a: unknown, b: unknown) => boolean) | undefined;
|
|
214
304
|
}
|
|
215
305
|
|
|
216
306
|
const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, (input: any) => Promise<any> | any>> = {
|