codify-plugin-lib 1.0.80 → 1.0.82
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/change-set.d.ts +1 -1
- package/dist/plan/change-set.js +6 -19
- package/dist/plan/plan.d.ts +1 -1
- package/dist/plan/plan.js +23 -11
- package/dist/resource/parsed-resource-settings.d.ts +0 -1
- package/dist/resource/parsed-resource-settings.js +2 -12
- package/dist/resource/resource-settings.d.ts +3 -3
- package/dist/resource/resource-settings.js +11 -2
- package/dist/utils/utils.d.ts +1 -6
- package/dist/utils/utils.js +2 -43
- package/package.json +2 -2
- package/src/plan/change-set.test.ts +100 -40
- package/src/plan/change-set.ts +10 -26
- package/src/plan/plan.test.ts +4 -5
- package/src/plan/plan.ts +29 -19
- package/src/resource/parsed-resource-settings.ts +2 -22
- package/src/resource/resource-controller.test.ts +3 -3
- package/src/resource/resource-settings.test.ts +1 -32
- package/src/resource/resource-settings.ts +17 -4
- package/src/resource/stateful-parameter.test.ts +60 -24
- package/src/utils/utils.ts +2 -57
|
@@ -30,7 +30,7 @@ export declare class ChangeSet<T extends StringIndexedObject> {
|
|
|
30
30
|
static empty<T extends StringIndexedObject>(): ChangeSet<T>;
|
|
31
31
|
static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T>;
|
|
32
32
|
static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T>;
|
|
33
|
-
static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>):
|
|
33
|
+
static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): ChangeSet<T>;
|
|
34
34
|
/**
|
|
35
35
|
* Calculates the differences between the desired and current parameters,
|
|
36
36
|
* and returns a list of parameter changes that describe what needs to be added,
|
package/dist/plan/change-set.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
2
|
-
import { areArraysEqual } from '../utils/utils.js';
|
|
3
2
|
// Change set will coerce undefined values to null because undefined is not valid JSON
|
|
4
3
|
export class ChangeSet {
|
|
5
4
|
operation;
|
|
@@ -45,8 +44,8 @@ export class ChangeSet {
|
|
|
45
44
|
}));
|
|
46
45
|
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
47
46
|
}
|
|
48
|
-
static
|
|
49
|
-
const pc =
|
|
47
|
+
static calculateModification(desired, current, parameterSettings = {}) {
|
|
48
|
+
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
50
49
|
const statefulParameterKeys = new Set(Object.entries(parameterSettings)
|
|
51
50
|
.filter(([, v]) => v?.type === 'stateful')
|
|
52
51
|
.map(([k]) => k));
|
|
@@ -77,7 +76,7 @@ export class ChangeSet {
|
|
|
77
76
|
* @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
|
|
78
77
|
* @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
|
|
79
78
|
*/
|
|
80
|
-
static
|
|
79
|
+
static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
|
|
81
80
|
const parameterChangeSet = new Array();
|
|
82
81
|
// Filter out null and undefined values or else the diff below will not work
|
|
83
82
|
const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
|
|
@@ -93,7 +92,7 @@ export class ChangeSet {
|
|
|
93
92
|
delete current[k];
|
|
94
93
|
continue;
|
|
95
94
|
}
|
|
96
|
-
if (!
|
|
95
|
+
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
97
96
|
parameterChangeSet.push({
|
|
98
97
|
name: k,
|
|
99
98
|
previousValue: v ?? null,
|
|
@@ -138,19 +137,7 @@ export class ChangeSet {
|
|
|
138
137
|
const indexNext = orderOfOperations.indexOf(next);
|
|
139
138
|
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
140
139
|
}
|
|
141
|
-
static
|
|
142
|
-
|
|
143
|
-
case 'stateful': {
|
|
144
|
-
const statefulSetting = setting.definition.getSettings();
|
|
145
|
-
return ChangeSet.isSame(desired, current, statefulSetting);
|
|
146
|
-
}
|
|
147
|
-
case 'array': {
|
|
148
|
-
const arrayParameter = setting;
|
|
149
|
-
return areArraysEqual(arrayParameter, desired, current);
|
|
150
|
-
}
|
|
151
|
-
default: {
|
|
152
|
-
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
140
|
+
static isSame(desired, current, setting) {
|
|
141
|
+
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
|
|
155
142
|
}
|
|
156
143
|
}
|
package/dist/plan/plan.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
41
41
|
coreParameters: ResourceConfig;
|
|
42
42
|
settings: ParsedResourceSettings<T>;
|
|
43
43
|
statefulMode: boolean;
|
|
44
|
-
}):
|
|
44
|
+
}): Plan<T>;
|
|
45
45
|
/**
|
|
46
46
|
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
47
47
|
* defined.
|
package/dist/plan/plan.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
|
|
2
2
|
import { v4 as uuidV4 } from 'uuid';
|
|
3
|
-
import { asyncFilter, asyncIncludes, asyncMap } from '../utils/utils.js';
|
|
4
3
|
import { ChangeSet } from './change-set.js';
|
|
5
4
|
/**
|
|
6
5
|
* A plan represents a set of actions that after taken will turn the current resource into the desired one.
|
|
@@ -71,7 +70,7 @@ export class Plan {
|
|
|
71
70
|
getResourceType() {
|
|
72
71
|
return this.coreParameters.type;
|
|
73
72
|
}
|
|
74
|
-
static
|
|
73
|
+
static calculate(params) {
|
|
75
74
|
const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
|
|
76
75
|
const currentParameters = Plan.matchCurrentParameters({
|
|
77
76
|
desiredParameters,
|
|
@@ -80,7 +79,7 @@ export class Plan {
|
|
|
80
79
|
settings,
|
|
81
80
|
statefulMode
|
|
82
81
|
});
|
|
83
|
-
const filteredCurrentParameters =
|
|
82
|
+
const filteredCurrentParameters = Plan.filterCurrentParams({
|
|
84
83
|
desiredParameters,
|
|
85
84
|
currentParameters,
|
|
86
85
|
stateParameters,
|
|
@@ -100,7 +99,7 @@ export class Plan {
|
|
|
100
99
|
return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
|
|
101
100
|
}
|
|
102
101
|
// NO-OP, MODIFY or RE-CREATE
|
|
103
|
-
const changeSet =
|
|
102
|
+
const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
|
|
104
103
|
return new Plan(uuidV4(), changeSet, coreParameters);
|
|
105
104
|
}
|
|
106
105
|
/**
|
|
@@ -111,7 +110,7 @@ export class Plan {
|
|
|
111
110
|
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
112
111
|
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
113
112
|
*/
|
|
114
|
-
static
|
|
113
|
+
static filterCurrentParams(params) {
|
|
115
114
|
const { desiredParameters: desired, currentParameters: current, stateParameters: state, settings, statefulMode } = params;
|
|
116
115
|
if (!current) {
|
|
117
116
|
return null;
|
|
@@ -126,8 +125,9 @@ export class Plan {
|
|
|
126
125
|
return filteredCurrent;
|
|
127
126
|
}
|
|
128
127
|
// TODO: Add object handling here in addition to arrays in the future
|
|
129
|
-
const arrayStatefulParameters = Object.fromEntries(
|
|
130
|
-
.filter(([k, v]) => isArrayStatefulParameter(k, v))
|
|
128
|
+
const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
|
|
129
|
+
.filter(([k, v]) => isArrayStatefulParameter(k, v))
|
|
130
|
+
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
|
|
131
131
|
return { ...filteredCurrent, ...arrayStatefulParameters };
|
|
132
132
|
function filterCurrent() {
|
|
133
133
|
if (!current) {
|
|
@@ -148,14 +148,26 @@ export class Plan {
|
|
|
148
148
|
&& settings.parameterSettings[k].definition.getSettings().type === 'array'
|
|
149
149
|
&& Array.isArray(v);
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
152
|
+
function filterArrayStatefulParameter(k, v) {
|
|
152
153
|
const desiredArray = desired[k];
|
|
153
154
|
const matcher = settings.parameterSettings[k]
|
|
154
155
|
.definition
|
|
155
156
|
.getSettings()
|
|
156
|
-
.isElementEqual;
|
|
157
|
-
const
|
|
158
|
-
|
|
157
|
+
.isElementEqual ?? ((a, b) => a === b);
|
|
158
|
+
const desiredCopy = [...desiredArray];
|
|
159
|
+
const currentCopy = [...v];
|
|
160
|
+
const result = [];
|
|
161
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
162
|
+
const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2));
|
|
163
|
+
if (idx === -1) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
desiredCopy.splice(counter, 1);
|
|
167
|
+
const [element] = currentCopy.splice(idx, 1);
|
|
168
|
+
result.push(element);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
173
|
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
@@ -21,6 +21,5 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
|
|
|
21
21
|
get statefulParameterOrder(): Map<keyof T, number>;
|
|
22
22
|
private validateSettings;
|
|
23
23
|
private validateParameterEqualsFn;
|
|
24
|
-
private resolveEqualsFn;
|
|
25
24
|
private getFromCacheOrCreate;
|
|
26
25
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ParameterEqualsDefaults } from './resource-settings.js';
|
|
1
|
+
import { resolveEqualsFn } from './resource-settings.js';
|
|
3
2
|
export class ParsedResourceSettings {
|
|
4
3
|
cache = new Map();
|
|
5
4
|
id;
|
|
@@ -35,7 +34,7 @@ export class ParsedResourceSettings {
|
|
|
35
34
|
const settings = Object.entries(this.settings.parameterSettings ?? {})
|
|
36
35
|
.map(([k, v]) => [k, v])
|
|
37
36
|
.map(([k, v]) => {
|
|
38
|
-
v.isEqual =
|
|
37
|
+
v.isEqual = resolveEqualsFn(v, k);
|
|
39
38
|
return [k, v];
|
|
40
39
|
});
|
|
41
40
|
return Object.fromEntries(settings);
|
|
@@ -106,15 +105,6 @@ export class ParsedResourceSettings {
|
|
|
106
105
|
}
|
|
107
106
|
// The rest of the types have defaults set already
|
|
108
107
|
}
|
|
109
|
-
resolveEqualsFn(parameter, key) {
|
|
110
|
-
if (parameter.type === 'array') {
|
|
111
|
-
return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter);
|
|
112
|
-
}
|
|
113
|
-
if (parameter.type === 'stateful') {
|
|
114
|
-
return this.resolveEqualsFn(parameter.definition.getSettings(), key);
|
|
115
|
-
}
|
|
116
|
-
return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
|
|
117
|
-
}
|
|
118
108
|
getFromCacheOrCreate(key, create) {
|
|
119
109
|
if (this.cache.has(key)) {
|
|
120
110
|
return this.cache.get(key);
|
|
@@ -97,7 +97,7 @@ export interface DefaultParameterSetting {
|
|
|
97
97
|
*
|
|
98
98
|
* @return Return true if equal
|
|
99
99
|
*/
|
|
100
|
-
isEqual?: (desired: any, current: any) =>
|
|
100
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
101
101
|
/**
|
|
102
102
|
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
103
103
|
* Defaults to false (re-create).
|
|
@@ -123,7 +123,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
123
123
|
*
|
|
124
124
|
* @return Return true if desired is equivalent to current.
|
|
125
125
|
*/
|
|
126
|
-
isElementEqual?: (desired: any, current: any) =>
|
|
126
|
+
isElementEqual?: (desired: any, current: any) => boolean;
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
129
|
* Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
|
|
@@ -146,4 +146,4 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
|
146
146
|
*/
|
|
147
147
|
order?: number;
|
|
148
148
|
}
|
|
149
|
-
export declare
|
|
149
|
+
export declare function resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { untildify } from '../utils/utils.js';
|
|
3
|
-
|
|
2
|
+
import { areArraysEqual, untildify } from '../utils/utils.js';
|
|
3
|
+
const ParameterEqualsDefaults = {
|
|
4
4
|
'boolean': (a, b) => Boolean(a) === Boolean(b),
|
|
5
5
|
'directory': (a, b) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
|
|
6
6
|
'number': (a, b) => Number(a) === Number(b),
|
|
7
7
|
'string': (a, b) => String(a) === String(b),
|
|
8
8
|
'version': (desired, current) => String(current).includes(String(desired))
|
|
9
9
|
};
|
|
10
|
+
export function resolveEqualsFn(parameter, key) {
|
|
11
|
+
if (parameter.type === 'array') {
|
|
12
|
+
return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter);
|
|
13
|
+
}
|
|
14
|
+
if (parameter.type === 'stateful') {
|
|
15
|
+
return resolveEqualsFn(parameter.definition.getSettings(), key);
|
|
16
|
+
}
|
|
17
|
+
return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
|
|
18
|
+
}
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -37,10 +37,5 @@ export declare function splitUserConfig<T extends StringIndexedObject>(config: R
|
|
|
37
37
|
};
|
|
38
38
|
export declare function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean;
|
|
39
39
|
export declare function untildify(pathWithTilde: string): string;
|
|
40
|
-
export declare function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown):
|
|
41
|
-
export declare function asyncFilter<T>(arr: T[], filter: (a: T) => Promise<boolean> | boolean): Promise<T[]>;
|
|
42
|
-
export declare function asyncMap<T, R>(arr: T[], map: (a: T) => Promise<R> | R): Promise<R[]>;
|
|
43
|
-
export declare function asyncFindIndex<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<number>;
|
|
44
|
-
export declare function asyncFind<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<T | undefined>;
|
|
45
|
-
export declare function asyncIncludes<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<boolean>;
|
|
40
|
+
export declare function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown): boolean;
|
|
46
41
|
export {};
|
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
|
|
75
|
+
export function areArraysEqual(parameter, desired, current) {
|
|
76
76
|
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
77
77
|
throw new Error(`A non-array value:
|
|
78
78
|
|
|
@@ -88,11 +88,10 @@ Was provided even though type array was specified.
|
|
|
88
88
|
}
|
|
89
89
|
const desiredCopy = [...desired];
|
|
90
90
|
const currentCopy = [...current];
|
|
91
|
-
const eq = parameter.isElementEqual ?? ((a, b) => a === b);
|
|
92
91
|
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
93
92
|
// an isElementEqual method. Time: O(n^2)
|
|
94
93
|
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
95
|
-
const idx =
|
|
94
|
+
const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2));
|
|
96
95
|
if (idx === -1) {
|
|
97
96
|
return false;
|
|
98
97
|
}
|
|
@@ -101,43 +100,3 @@ Was provided even though type array was specified.
|
|
|
101
100
|
}
|
|
102
101
|
return currentCopy.length === 0;
|
|
103
102
|
}
|
|
104
|
-
export async function asyncFilter(arr, filter) {
|
|
105
|
-
const result = [];
|
|
106
|
-
for (const element of arr) {
|
|
107
|
-
if (await filter(element)) {
|
|
108
|
-
result.push(element);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
export async function asyncMap(arr, map) {
|
|
114
|
-
const result = [];
|
|
115
|
-
for (const element of arr) {
|
|
116
|
-
result.push(await map(element));
|
|
117
|
-
}
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
export async function asyncFindIndex(arr, eq) {
|
|
121
|
-
for (const [counter, element] of arr.entries()) {
|
|
122
|
-
if (await eq(element)) {
|
|
123
|
-
return counter;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return -1;
|
|
127
|
-
}
|
|
128
|
-
export async function asyncFind(arr, eq) {
|
|
129
|
-
for (const element of arr) {
|
|
130
|
-
if (await eq(element)) {
|
|
131
|
-
return element;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
export async function asyncIncludes(arr, eq) {
|
|
137
|
-
for (const element of arr) {
|
|
138
|
-
if (await eq(element)) {
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.82",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@oclif/prettier-config": "^0.2.1",
|
|
23
23
|
"@oclif/test": "^3",
|
|
24
24
|
"@types/npmcli__promise-spawn": "^6.0.3",
|
|
25
|
-
"@types/node": "^
|
|
25
|
+
"@types/node": "^20",
|
|
26
26
|
"@types/semver": "^7.5.4",
|
|
27
27
|
"@types/sinon": "^17.0.3",
|
|
28
28
|
"@types/uuid": "^10.0.0",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ChangeSet } from './change-set.js';
|
|
2
2
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
4
5
|
|
|
5
|
-
describe('Change set tests',
|
|
6
|
-
it ('Correctly diffs two resource configs (modify)',
|
|
6
|
+
describe('Change set tests', () => {
|
|
7
|
+
it ('Correctly diffs two resource configs (modify)', () => {
|
|
7
8
|
const after = {
|
|
8
9
|
propA: 'before',
|
|
9
10
|
propB: 'before'
|
|
@@ -14,14 +15,14 @@ describe('Change set tests', async () => {
|
|
|
14
15
|
propB: 'after'
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const cs =
|
|
18
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
18
19
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
19
20
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
20
21
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.MODIFY);
|
|
21
22
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
22
23
|
})
|
|
23
24
|
|
|
24
|
-
it ('Correctly diffs two resource configs (add)',
|
|
25
|
+
it ('Correctly diffs two resource configs (add)', () => {
|
|
25
26
|
const after = {
|
|
26
27
|
propA: 'before',
|
|
27
28
|
propB: 'after'
|
|
@@ -31,7 +32,7 @@ describe('Change set tests', async () => {
|
|
|
31
32
|
propA: 'after',
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const cs =
|
|
35
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
35
36
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
36
37
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
37
38
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.ADD);
|
|
@@ -39,7 +40,7 @@ describe('Change set tests', async () => {
|
|
|
39
40
|
|
|
40
41
|
})
|
|
41
42
|
|
|
42
|
-
it ('Correctly diffs two resource configs (remove)',
|
|
43
|
+
it ('Correctly diffs two resource configs (remove)', () => {
|
|
43
44
|
const after = {
|
|
44
45
|
propA: 'after',
|
|
45
46
|
}
|
|
@@ -49,14 +50,14 @@ describe('Change set tests', async () => {
|
|
|
49
50
|
propB: 'before'
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const cs =
|
|
53
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
53
54
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
54
55
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
55
56
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
56
57
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
57
58
|
})
|
|
58
59
|
|
|
59
|
-
it ('Correctly diffs two resource configs (no-op)',
|
|
60
|
+
it ('Correctly diffs two resource configs (no-op)', () => {
|
|
60
61
|
const after = {
|
|
61
62
|
propA: 'prop',
|
|
62
63
|
}
|
|
@@ -65,7 +66,7 @@ describe('Change set tests', async () => {
|
|
|
65
66
|
propA: 'prop',
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const cs =
|
|
69
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
69
70
|
expect(cs.parameterChanges.length).to.eq(1);
|
|
70
71
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
71
72
|
expect(cs.operation).to.eq(ResourceOperation.NOOP)
|
|
@@ -95,7 +96,7 @@ describe('Change set tests', async () => {
|
|
|
95
96
|
expect(cs.operation).to.eq(ResourceOperation.DESTROY)
|
|
96
97
|
})
|
|
97
98
|
|
|
98
|
-
it ('handles simple arrays',
|
|
99
|
+
it ('handles simple arrays', () => {
|
|
99
100
|
const before = {
|
|
100
101
|
propA: ['a', 'b', 'c'],
|
|
101
102
|
}
|
|
@@ -104,13 +105,20 @@ describe('Change set tests', async () => {
|
|
|
104
105
|
propA: ['b', 'a', 'c'],
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
const
|
|
108
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
109
|
+
id: 'type',
|
|
110
|
+
parameterSettings: {
|
|
111
|
+
propA: { type: 'array' }
|
|
112
|
+
}
|
|
113
|
+
}).parameterSettings
|
|
114
|
+
|
|
115
|
+
const cs = ChangeSet.calculateModification(after, before, parameterSettings);
|
|
108
116
|
expect(cs.parameterChanges.length).to.eq(1);
|
|
109
117
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
110
118
|
expect(cs.operation).to.eq(ResourceOperation.NOOP)
|
|
111
119
|
})
|
|
112
120
|
|
|
113
|
-
it('handles simple arrays 2',
|
|
121
|
+
it('handles simple arrays 2', () => {
|
|
114
122
|
const after = {
|
|
115
123
|
propA: ['a', 'b', 'c'],
|
|
116
124
|
}
|
|
@@ -119,13 +127,20 @@ describe('Change set tests', async () => {
|
|
|
119
127
|
propA: ['b', 'a'],
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
const
|
|
130
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
131
|
+
id: 'type',
|
|
132
|
+
parameterSettings: {
|
|
133
|
+
propA: { type: 'array' }
|
|
134
|
+
}
|
|
135
|
+
}).parameterSettings
|
|
136
|
+
|
|
137
|
+
const cs = ChangeSet.calculateModification(after, before, parameterSettings);
|
|
123
138
|
expect(cs.parameterChanges.length).to.eq(1);
|
|
124
139
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
125
140
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
126
141
|
})
|
|
127
142
|
|
|
128
|
-
it('determines the order of operations with canModify 1',
|
|
143
|
+
it('determines the order of operations with canModify 1', () => {
|
|
129
144
|
const after = {
|
|
130
145
|
propA: 'after',
|
|
131
146
|
}
|
|
@@ -135,14 +150,21 @@ describe('Change set tests', async () => {
|
|
|
135
150
|
propB: 'before'
|
|
136
151
|
}
|
|
137
152
|
|
|
138
|
-
const
|
|
153
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
154
|
+
id: 'type',
|
|
155
|
+
parameterSettings: {
|
|
156
|
+
propA: { canModify: true }
|
|
157
|
+
}
|
|
158
|
+
}).parameterSettings
|
|
159
|
+
|
|
160
|
+
const cs = ChangeSet.calculateModification(after, before, parameterSettings);
|
|
139
161
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
140
162
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
141
163
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
142
164
|
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
143
165
|
})
|
|
144
166
|
|
|
145
|
-
it('determines the order of operations with canModify 2',
|
|
167
|
+
it('determines the order of operations with canModify 2', () => {
|
|
146
168
|
const after = {
|
|
147
169
|
propA: 'after',
|
|
148
170
|
}
|
|
@@ -152,10 +174,15 @@ describe('Change set tests', async () => {
|
|
|
152
174
|
propB: 'before'
|
|
153
175
|
}
|
|
154
176
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
178
|
+
id: 'type',
|
|
179
|
+
parameterSettings: {
|
|
180
|
+
propA: { canModify: true },
|
|
181
|
+
propB: { canModify: true }
|
|
182
|
+
},
|
|
183
|
+
}).parameterSettings
|
|
184
|
+
|
|
185
|
+
const cs = ChangeSet.calculateModification<any>(after, before, parameterSettings);
|
|
159
186
|
expect(cs.parameterChanges.length).to.eq(2);
|
|
160
187
|
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
161
188
|
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
@@ -163,57 +190,90 @@ describe('Change set tests', async () => {
|
|
|
163
190
|
})
|
|
164
191
|
|
|
165
192
|
|
|
166
|
-
it('correctly determines array equality',
|
|
193
|
+
it('correctly determines array equality', () => {
|
|
167
194
|
const arrA = ['a', 'b', 'd'];
|
|
168
195
|
const arrB = ['a', 'b', 'd'];
|
|
169
196
|
|
|
170
|
-
const
|
|
197
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
198
|
+
id: 'type',
|
|
199
|
+
parameterSettings: {
|
|
200
|
+
propA: { type: 'array' }
|
|
201
|
+
},
|
|
202
|
+
}).parameterSettings
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
|
|
171
206
|
|
|
172
207
|
expect(result.operation).to.eq(ResourceOperation.NOOP);
|
|
173
208
|
})
|
|
174
209
|
|
|
175
|
-
it('correctly determines array equality 2',
|
|
210
|
+
it('correctly determines array equality 2', () => {
|
|
176
211
|
const arrA = ['a', 'b'];
|
|
177
212
|
const arrB = ['a', 'b', 'd'];
|
|
178
213
|
|
|
179
|
-
const
|
|
214
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
215
|
+
id: 'type',
|
|
216
|
+
parameterSettings: {
|
|
217
|
+
propA: { type: 'array' }
|
|
218
|
+
},
|
|
219
|
+
}).parameterSettings
|
|
220
|
+
|
|
221
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
|
|
180
222
|
|
|
181
223
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
182
224
|
})
|
|
183
225
|
|
|
184
|
-
it('correctly determines array equality 3',
|
|
226
|
+
it('correctly determines array equality 3', () => {
|
|
185
227
|
const arrA = ['b', 'a', 'd'];
|
|
186
228
|
const arrB = ['a', 'b', 'd'];
|
|
187
229
|
|
|
188
|
-
const
|
|
230
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
231
|
+
id: 'type',
|
|
232
|
+
parameterSettings: {
|
|
233
|
+
propA: { type: 'array' }
|
|
234
|
+
},
|
|
235
|
+
}).parameterSettings
|
|
236
|
+
|
|
237
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
|
|
189
238
|
|
|
190
239
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
191
240
|
})
|
|
192
241
|
|
|
193
|
-
it('correctly determines array equality 4',
|
|
242
|
+
it('correctly determines array equality 4', () => {
|
|
194
243
|
const arrA = [{ key1: 'a' }, { key1: 'a' }, { key1: 'a' }];
|
|
195
244
|
const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
|
|
196
245
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
246
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
247
|
+
id: 'type',
|
|
248
|
+
parameterSettings: {
|
|
249
|
+
propA: {
|
|
250
|
+
type: 'array',
|
|
251
|
+
isElementEqual: (a, b) => a.key1 === b.key1
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
}).parameterSettings
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
|
|
203
258
|
|
|
204
259
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
205
260
|
})
|
|
206
261
|
|
|
207
|
-
it('correctly determines array equality 5',
|
|
262
|
+
it('correctly determines array equality 5', () => {
|
|
208
263
|
const arrA = [{ key1: 'b' }, { key1: 'a' }, { key1: 'a' }];
|
|
209
264
|
const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
|
|
210
265
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
266
|
+
const parameterSettings = new ParsedResourceSettings({
|
|
267
|
+
id: 'type',
|
|
268
|
+
parameterSettings: {
|
|
269
|
+
propA: {
|
|
270
|
+
type: 'array',
|
|
271
|
+
isElementEqual: (a, b) => a.key1 === b.key1
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
}).parameterSettings
|
|
275
|
+
|
|
276
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, parameterSettings)
|
|
217
277
|
|
|
218
278
|
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
219
279
|
})
|
package/src/plan/change-set.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { areArraysEqual } from '../utils/utils.js';
|
|
3
|
+
import { ParameterSetting } from '../resource/resource-settings.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* A parameter change describes a parameter level change to a resource.
|
|
@@ -85,12 +84,12 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
85
84
|
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
static
|
|
87
|
+
static calculateModification<T extends StringIndexedObject>(
|
|
89
88
|
desired: Partial<T>,
|
|
90
89
|
current: Partial<T>,
|
|
91
90
|
parameterSettings: Partial<Record<keyof T, ParameterSetting>> = {},
|
|
92
|
-
):
|
|
93
|
-
const pc =
|
|
91
|
+
): ChangeSet<T> {
|
|
92
|
+
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
94
93
|
|
|
95
94
|
const statefulParameterKeys = new Set(
|
|
96
95
|
Object.entries(parameterSettings)
|
|
@@ -126,11 +125,11 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
126
125
|
* @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
|
|
127
126
|
* @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
|
|
128
127
|
*/
|
|
129
|
-
private static
|
|
128
|
+
private static calculateParameterChanges<T extends StringIndexedObject>(
|
|
130
129
|
desiredParameters: Partial<T>,
|
|
131
130
|
currentParameters: Partial<T>,
|
|
132
131
|
parameterOptions?: Partial<Record<keyof T, ParameterSetting>>,
|
|
133
|
-
):
|
|
132
|
+
): ParameterChange<T>[] {
|
|
134
133
|
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
135
134
|
|
|
136
135
|
// Filter out null and undefined values or else the diff below will not work
|
|
@@ -155,7 +154,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
155
154
|
continue;
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
if (!
|
|
157
|
+
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
159
158
|
parameterChangeSet.push({
|
|
160
159
|
name: k,
|
|
161
160
|
previousValue: v ?? null,
|
|
@@ -210,26 +209,11 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
210
209
|
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
211
210
|
}
|
|
212
211
|
|
|
213
|
-
private static
|
|
212
|
+
private static isSame(
|
|
214
213
|
desired: unknown,
|
|
215
214
|
current: unknown,
|
|
216
215
|
setting?: ParameterSetting,
|
|
217
|
-
):
|
|
218
|
-
|
|
219
|
-
case 'stateful': {
|
|
220
|
-
const statefulSetting = (setting as StatefulParameterSetting).definition.getSettings()
|
|
221
|
-
|
|
222
|
-
return ChangeSet.isSame(desired, current, statefulSetting as ParameterSetting);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
case 'array': {
|
|
226
|
-
const arrayParameter = setting as ArrayParameterSetting;
|
|
227
|
-
return areArraysEqual(arrayParameter, desired, current)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
default: {
|
|
231
|
-
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
216
|
+
): boolean {
|
|
217
|
+
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current)
|
|
234
218
|
}
|
|
235
219
|
}
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Plan } from './plan.js';
|
|
3
3
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
4
|
-
import { TestConfig,
|
|
4
|
+
import { TestConfig, TestResource } from '../utils/test-utils.test.js';
|
|
5
5
|
import { ResourceController } from '../resource/resource-controller.js';
|
|
6
6
|
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
7
7
|
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
@@ -128,8 +128,8 @@ describe('Plan entity tests', () => {
|
|
|
128
128
|
).to.be.true;
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
it('Returns the original resource names',
|
|
132
|
-
const plan =
|
|
131
|
+
it('Returns the original resource names', () => {
|
|
132
|
+
const plan = Plan.calculate<TestConfig>({
|
|
133
133
|
desiredParameters: { propA: 'propA' },
|
|
134
134
|
currentParametersArray: [{ propA: 'propA2' }],
|
|
135
135
|
stateParameters: null,
|
|
@@ -147,8 +147,7 @@ describe('Plan entity tests', () => {
|
|
|
147
147
|
operation: ResourceOperation.RECREATE
|
|
148
148
|
})
|
|
149
149
|
})
|
|
150
|
-
})
|
|
151
|
-
|
|
150
|
+
})
|
|
152
151
|
|
|
153
152
|
function createTestResource() {
|
|
154
153
|
return new class extends TestResource {
|
package/src/plan/plan.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { v4 as uuidV4 } from 'uuid';
|
|
|
10
10
|
|
|
11
11
|
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
12
12
|
import { ArrayParameterSetting, ResourceSettings, StatefulParameterSetting } from '../resource/resource-settings.js';
|
|
13
|
-
import { asyncFilter, asyncIncludes, asyncMap } from '../utils/utils.js';
|
|
14
13
|
import { ChangeSet } from './change-set.js';
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -106,14 +105,14 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
106
105
|
return this.coreParameters.type
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
static
|
|
108
|
+
static calculate<T extends StringIndexedObject>(params: {
|
|
110
109
|
desiredParameters: Partial<T> | null,
|
|
111
110
|
currentParametersArray: Partial<T>[] | null,
|
|
112
111
|
stateParameters: Partial<T> | null,
|
|
113
112
|
coreParameters: ResourceConfig,
|
|
114
113
|
settings: ParsedResourceSettings<T>,
|
|
115
114
|
statefulMode: boolean,
|
|
116
|
-
}):
|
|
115
|
+
}): Plan<T> {
|
|
117
116
|
const {
|
|
118
117
|
desiredParameters,
|
|
119
118
|
currentParametersArray,
|
|
@@ -131,7 +130,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
131
130
|
statefulMode
|
|
132
131
|
});
|
|
133
132
|
|
|
134
|
-
const filteredCurrentParameters =
|
|
133
|
+
const filteredCurrentParameters = Plan.filterCurrentParams<T>({
|
|
135
134
|
desiredParameters,
|
|
136
135
|
currentParameters,
|
|
137
136
|
stateParameters,
|
|
@@ -167,7 +166,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
167
166
|
}
|
|
168
167
|
|
|
169
168
|
// NO-OP, MODIFY or RE-CREATE
|
|
170
|
-
const changeSet =
|
|
169
|
+
const changeSet = ChangeSet.calculateModification(
|
|
171
170
|
desiredParameters!,
|
|
172
171
|
filteredCurrentParameters!,
|
|
173
172
|
settings.parameterSettings,
|
|
@@ -188,13 +187,13 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
188
187
|
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
189
188
|
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
190
189
|
*/
|
|
191
|
-
private static
|
|
190
|
+
private static filterCurrentParams<T extends StringIndexedObject>(params: {
|
|
192
191
|
desiredParameters: Partial<T> | null,
|
|
193
192
|
currentParameters: Partial<T> | null,
|
|
194
193
|
stateParameters: Partial<T> | null,
|
|
195
194
|
settings: ResourceSettings<T>,
|
|
196
195
|
statefulMode: boolean,
|
|
197
|
-
}):
|
|
196
|
+
}): Partial<T> | null {
|
|
198
197
|
const {
|
|
199
198
|
desiredParameters: desired,
|
|
200
199
|
currentParameters: current,
|
|
@@ -220,12 +219,10 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
220
219
|
|
|
221
220
|
// TODO: Add object handling here in addition to arrays in the future
|
|
222
221
|
const arrayStatefulParameters = Object.fromEntries(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
228
|
-
);
|
|
222
|
+
Object.entries(filteredCurrent)
|
|
223
|
+
.filter(([k, v]) => isArrayStatefulParameter(k, v))
|
|
224
|
+
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
|
|
225
|
+
)
|
|
229
226
|
|
|
230
227
|
return { ...filteredCurrent, ...arrayStatefulParameters }
|
|
231
228
|
|
|
@@ -256,18 +253,31 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
256
253
|
&& Array.isArray(v)
|
|
257
254
|
}
|
|
258
255
|
|
|
259
|
-
|
|
256
|
+
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
257
|
+
function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
|
|
260
258
|
const desiredArray = desired![k] as unknown[];
|
|
261
259
|
const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
|
|
262
260
|
.definition
|
|
263
261
|
.getSettings() as ArrayParameterSetting)
|
|
264
|
-
.isElementEqual;
|
|
262
|
+
.isElementEqual ?? ((a, b) => a === b);
|
|
265
263
|
|
|
266
|
-
const
|
|
264
|
+
const desiredCopy = [...desiredArray];
|
|
265
|
+
const currentCopy = [...v];
|
|
266
|
+
const result = [];
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
269
|
+
const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2))
|
|
270
|
+
|
|
271
|
+
if (idx === -1) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
desiredCopy.splice(counter, 1)
|
|
276
|
+
const [element] = currentCopy.splice(idx, 1)
|
|
277
|
+
result.push(element)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
271
281
|
}
|
|
272
282
|
}
|
|
273
283
|
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
ArrayParameterSetting,
|
|
6
|
-
ParameterEqualsDefaults,
|
|
7
|
-
ParameterSetting,
|
|
8
|
-
ParameterSettingType,
|
|
9
|
-
ResourceSettings,
|
|
10
|
-
StatefulParameterSetting
|
|
11
|
-
} from './resource-settings.js';
|
|
3
|
+
import { ParameterSetting, resolveEqualsFn, ResourceSettings, StatefulParameterSetting } from './resource-settings.js';
|
|
12
4
|
import { StatefulParameter as StatefulParameterImpl } from './stateful-parameter.js'
|
|
13
5
|
|
|
14
6
|
export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
|
|
@@ -54,7 +46,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
54
46
|
const settings = Object.entries(this.settings.parameterSettings ?? {})
|
|
55
47
|
.map(([k, v]) => [k, v!] as const)
|
|
56
48
|
.map(([k, v]) => {
|
|
57
|
-
v.isEqual =
|
|
49
|
+
v.isEqual = resolveEqualsFn(v, k);
|
|
58
50
|
|
|
59
51
|
return [k, v];
|
|
60
52
|
})
|
|
@@ -154,18 +146,6 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
154
146
|
// The rest of the types have defaults set already
|
|
155
147
|
}
|
|
156
148
|
|
|
157
|
-
private resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => Promise<boolean> | boolean {
|
|
158
|
-
if (parameter.type === 'array') {
|
|
159
|
-
return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (parameter.type === 'stateful') {
|
|
163
|
-
return this.resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings(), key)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
149
|
private getFromCacheOrCreate<T2>(key: string, create: () => T2): T2 {
|
|
170
150
|
if (this.cache.has(key)) {
|
|
171
151
|
return this.cache.get(key) as T2
|
|
@@ -99,7 +99,7 @@ describe('Resource tests', () => {
|
|
|
99
99
|
const resourceSpy = spy(resource);
|
|
100
100
|
|
|
101
101
|
await controllerSpy.apply(
|
|
102
|
-
|
|
102
|
+
testPlan({
|
|
103
103
|
desired: { propA: 'a', propB: 0 },
|
|
104
104
|
})
|
|
105
105
|
)
|
|
@@ -116,7 +116,7 @@ describe('Resource tests', () => {
|
|
|
116
116
|
const resourceSpy = spy(resource);
|
|
117
117
|
|
|
118
118
|
await controllerSpy.apply(
|
|
119
|
-
|
|
119
|
+
testPlan({
|
|
120
120
|
current: [{ propA: 'a', propB: 0 }],
|
|
121
121
|
state: { propA: 'a', propB: 0 },
|
|
122
122
|
statefulMode: true,
|
|
@@ -135,7 +135,7 @@ describe('Resource tests', () => {
|
|
|
135
135
|
const resourceSpy = spy(resource);
|
|
136
136
|
|
|
137
137
|
await controllerSpy.apply(
|
|
138
|
-
|
|
138
|
+
testPlan({
|
|
139
139
|
desired: { propA: 'a', propB: 0 },
|
|
140
140
|
current: [{ propA: 'b', propB: -1 }],
|
|
141
141
|
statefulMode: true
|
|
@@ -77,7 +77,7 @@ describe('Resource parameter tests', () => {
|
|
|
77
77
|
const resourceSpy = spy(resource);
|
|
78
78
|
|
|
79
79
|
await controller.apply(
|
|
80
|
-
|
|
80
|
+
testPlan<TestConfig>({
|
|
81
81
|
desired: { propA: 'a', propB: 0, propC: 'c' }
|
|
82
82
|
})
|
|
83
83
|
);
|
|
@@ -491,35 +491,4 @@ describe('Resource parameter tests', () => {
|
|
|
491
491
|
|
|
492
492
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
493
493
|
})
|
|
494
|
-
|
|
495
|
-
it('Works with async equals methods', async () => {
|
|
496
|
-
const resource = new class extends TestResource {
|
|
497
|
-
getSettings(): ResourceSettings<TestConfig> {
|
|
498
|
-
return {
|
|
499
|
-
id: 'type',
|
|
500
|
-
parameterSettings: {
|
|
501
|
-
propA: {
|
|
502
|
-
isEqual: async (desired, current) => {
|
|
503
|
-
console.log(desired, current)
|
|
504
|
-
await sleep(500);
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
};
|
|
512
|
-
const controller = new ResourceController(resource);
|
|
513
|
-
|
|
514
|
-
const plan = await controller.plan({ type: 'type', propA: 'abc' } as any);
|
|
515
|
-
|
|
516
|
-
console.log(JSON.stringify(plan, null, 2));
|
|
517
|
-
|
|
518
|
-
expect(plan.toResponse().operation).to.equal(ResourceOperation.NOOP);
|
|
519
|
-
})
|
|
520
494
|
})
|
|
521
|
-
|
|
522
|
-
function sleep(ms: number) {
|
|
523
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
524
|
-
}
|
|
525
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StringIndexedObject } from 'codify-schemas';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { untildify } from '../utils/utils.js';
|
|
4
|
+
import { areArraysEqual, untildify } from '../utils/utils.js';
|
|
5
5
|
import { StatefulParameter } from './stateful-parameter.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -127,7 +127,7 @@ export interface DefaultParameterSetting {
|
|
|
127
127
|
*
|
|
128
128
|
* @return Return true if equal
|
|
129
129
|
*/
|
|
130
|
-
isEqual?: (desired: any, current: any) =>
|
|
130
|
+
isEqual?: (desired: any, current: any) => boolean;
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Chose if the resource can be modified instead of re-created when there is a change to this parameter.
|
|
@@ -156,7 +156,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
156
156
|
*
|
|
157
157
|
* @return Return true if desired is equivalent to current.
|
|
158
158
|
*/
|
|
159
|
-
isElementEqual?: (desired: any, current: any) =>
|
|
159
|
+
isElementEqual?: (desired: any, current: any) => boolean
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
@@ -183,10 +183,23 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
|
183
183
|
order?: number,
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
|
|
187
187
|
'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
|
|
188
188
|
'directory': (a: unknown, b: unknown) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
|
|
189
189
|
'number': (a: unknown, b: unknown) => Number(a) === Number(b),
|
|
190
190
|
'string': (a: unknown, b: unknown) => String(a) === String(b),
|
|
191
191
|
'version': (desired: unknown, current: unknown) => String(current).includes(String(desired))
|
|
192
192
|
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
export function resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean {
|
|
196
|
+
if (parameter.type === 'array') {
|
|
197
|
+
return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (parameter.type === 'stateful') {
|
|
201
|
+
return resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings(), key)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
|
|
205
|
+
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { spy } from 'sinon';
|
|
3
3
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
TestArrayStatefulParameter,
|
|
6
|
+
TestConfig,
|
|
7
|
+
testPlan,
|
|
8
|
+
TestResource,
|
|
9
|
+
TestStatefulParameter
|
|
10
|
+
} from '../utils/test-utils.test.js';
|
|
11
|
+
import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
|
|
6
12
|
import { ResourceController } from './resource-controller.js';
|
|
7
13
|
|
|
8
14
|
describe('Stateful parameter tests', () => {
|
|
9
15
|
it('addItem is called the correct number of times', async () => {
|
|
10
|
-
const plan =
|
|
16
|
+
const plan = testPlan<TestConfig>({
|
|
11
17
|
desired: { propZ: ['a', 'b', 'c'] },
|
|
12
18
|
});
|
|
13
19
|
|
|
@@ -22,7 +28,7 @@ describe('Stateful parameter tests', () => {
|
|
|
22
28
|
})
|
|
23
29
|
|
|
24
30
|
it('applyRemoveItem is called the correct number of times', async () => {
|
|
25
|
-
const plan =
|
|
31
|
+
const plan = testPlan<TestConfig>({
|
|
26
32
|
desired: null,
|
|
27
33
|
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
28
34
|
state: { propZ: ['a', 'b', 'c'] },
|
|
@@ -41,7 +47,7 @@ describe('Stateful parameter tests', () => {
|
|
|
41
47
|
|
|
42
48
|
it('In stateless mode only applyAddItem is called only for modifies', async () => {
|
|
43
49
|
const parameter = new TestArrayStatefulParameter()
|
|
44
|
-
const plan =
|
|
50
|
+
const plan = testPlan<TestConfig>({
|
|
45
51
|
desired: { propZ: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
|
|
46
52
|
current: [{ propZ: ['a', 'b', 'c'] }],
|
|
47
53
|
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: parameter } } },
|
|
@@ -50,7 +56,7 @@ describe('Stateful parameter tests', () => {
|
|
|
50
56
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
|
|
51
57
|
expect(plan.changeSet.parameterChanges[0]).toMatchObject({
|
|
52
58
|
name: 'propZ',
|
|
53
|
-
previousValue: ['
|
|
59
|
+
previousValue: ['c', 'a'], // In stateless mode the previous value gets filtered to prevent deletes
|
|
54
60
|
newValue: ['a', 'c', 'd', 'e', 'f'],
|
|
55
61
|
operation: ParameterOperation.MODIFY,
|
|
56
62
|
})
|
|
@@ -72,7 +78,7 @@ describe('Stateful parameter tests', () => {
|
|
|
72
78
|
}
|
|
73
79
|
});
|
|
74
80
|
|
|
75
|
-
const plan =
|
|
81
|
+
const plan = testPlan<TestConfig>({
|
|
76
82
|
desired: { propZ: ['9.12', '9.13'] }, // b to remove, d, e, f to add
|
|
77
83
|
current: [{ propZ: ['9.12.9'] }],
|
|
78
84
|
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
|
|
@@ -92,31 +98,61 @@ describe('Stateful parameter tests', () => {
|
|
|
92
98
|
expect(testParameter.removeItem.called).to.be.false;
|
|
93
99
|
})
|
|
94
100
|
|
|
95
|
-
it('
|
|
96
|
-
const testParameter = spy(new class extends
|
|
97
|
-
getSettings():
|
|
101
|
+
it('isEqual works with type defaults', () => {
|
|
102
|
+
const testParameter = spy(new class extends TestStatefulParameter {
|
|
103
|
+
getSettings(): ParameterSetting {
|
|
98
104
|
return {
|
|
99
|
-
type: '
|
|
100
|
-
isElementEqual: async (desired, current) => {
|
|
101
|
-
console.log(desired, current)
|
|
102
|
-
await sleep(50);
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
+
type: 'version',
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
const plan =
|
|
110
|
-
desired: { propZ:
|
|
111
|
-
current: [{ propZ:
|
|
110
|
+
const plan = testPlan<TestConfig>({
|
|
111
|
+
desired: { propZ: '20' },
|
|
112
|
+
current: [{ propZ: '20.17.0' }],
|
|
112
113
|
settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
expect(plan.
|
|
116
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
116
117
|
})
|
|
117
|
-
})
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
it('isElementEquals test', async () => {
|
|
120
|
+
const testParameter = spy(new class extends TestArrayStatefulParameter {
|
|
121
|
+
getSettings(): ArrayParameterSetting {
|
|
122
|
+
return {
|
|
123
|
+
type: 'array',
|
|
124
|
+
isElementEqual: (desired, current) => current.includes(desired),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async refresh(): Promise<any> {
|
|
129
|
+
return [
|
|
130
|
+
'20.15.0',
|
|
131
|
+
'20.15.1'
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const resource = new class extends TestResource {
|
|
137
|
+
getSettings(): ResourceSettings<any> {
|
|
138
|
+
return {
|
|
139
|
+
id: 'type',
|
|
140
|
+
parameterSettings: { nodeVersions: { type: 'stateful', definition: testParameter } }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
122
143
|
|
|
144
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const controller = new ResourceController(resource);
|
|
150
|
+
const plan = await controller.plan({
|
|
151
|
+
nodeVersions: ['20.15'],
|
|
152
|
+
} as any)
|
|
153
|
+
|
|
154
|
+
console.log(JSON.stringify(plan, null, 2))
|
|
155
|
+
|
|
156
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
157
|
+
})
|
|
158
|
+
})
|
package/src/utils/utils.ts
CHANGED
|
@@ -111,7 +111,7 @@ export function untildify(pathWithTilde: string) {
|
|
|
111
111
|
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export
|
|
114
|
+
export function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
|
|
115
115
|
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
116
116
|
throw new Error(`A non-array value:
|
|
117
117
|
|
|
@@ -130,12 +130,10 @@ Was provided even though type array was specified.
|
|
|
130
130
|
const desiredCopy = [...desired];
|
|
131
131
|
const currentCopy = [...current];
|
|
132
132
|
|
|
133
|
-
const eq = parameter.isElementEqual ?? ((a, b) => a === b);
|
|
134
|
-
|
|
135
133
|
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
136
134
|
// an isElementEqual method. Time: O(n^2)
|
|
137
135
|
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
138
|
-
const idx =
|
|
136
|
+
const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2))
|
|
139
137
|
|
|
140
138
|
if (idx === -1) {
|
|
141
139
|
return false;
|
|
@@ -147,56 +145,3 @@ Was provided even though type array was specified.
|
|
|
147
145
|
|
|
148
146
|
return currentCopy.length === 0;
|
|
149
147
|
}
|
|
150
|
-
|
|
151
|
-
export async function asyncFilter<T>(arr: T[], filter: (a: T) => Promise<boolean> | boolean): Promise<T[]> {
|
|
152
|
-
const result = [];
|
|
153
|
-
|
|
154
|
-
for (const element of arr) {
|
|
155
|
-
if (await filter(element)) {
|
|
156
|
-
result.push(element);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function asyncMap<T, R>(arr: T[], map: (a: T) => Promise<R> | R): Promise<R[]> {
|
|
164
|
-
const result: R[] = [];
|
|
165
|
-
|
|
166
|
-
for (const element of arr) {
|
|
167
|
-
result.push(await map(element));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
export async function asyncFindIndex<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<number> {
|
|
175
|
-
for (const [counter, element] of arr.entries()) {
|
|
176
|
-
if (await eq(element)) {
|
|
177
|
-
return counter;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return -1;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export async function asyncFind<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<T | undefined> {
|
|
185
|
-
for (const element of arr) {
|
|
186
|
-
if (await eq(element)) {
|
|
187
|
-
return element;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return undefined;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export async function asyncIncludes<T>(arr: T[], eq: (a: T) => Promise<boolean> | boolean): Promise<boolean> {
|
|
195
|
-
for (const element of arr) {
|
|
196
|
-
if (await eq(element)) {
|
|
197
|
-
return true
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return false;
|
|
202
|
-
}
|