codify-plugin-lib 1.0.76 → 1.0.77
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/.eslintrc.json +11 -4
- package/.github/workflows/release.yaml +19 -0
- package/.github/workflows/unit-test-ci.yaml +19 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +9 -9
- package/dist/messages/handlers.d.ts +1 -1
- package/dist/messages/handlers.js +2 -1
- package/dist/plan/change-set.d.ts +37 -0
- package/dist/plan/change-set.js +146 -0
- package/dist/plan/plan-types.d.ts +23 -0
- package/dist/plan/plan-types.js +1 -0
- package/dist/plan/plan.d.ts +59 -0
- package/dist/plan/plan.js +228 -0
- package/dist/plugin/plugin.d.ts +17 -0
- package/dist/plugin/plugin.js +83 -0
- package/dist/resource/config-parser.d.ts +14 -0
- package/dist/resource/config-parser.js +48 -0
- package/dist/resource/parsed-resource-settings.d.ts +26 -0
- package/dist/resource/parsed-resource-settings.js +126 -0
- package/dist/resource/resource-controller.d.ts +30 -0
- package/dist/resource/resource-controller.js +247 -0
- package/dist/resource/resource-settings.d.ts +149 -0
- package/dist/resource/resource-settings.js +9 -0
- package/dist/resource/resource.d.ts +137 -0
- package/dist/resource/resource.js +44 -0
- package/dist/resource/stateful-parameter.d.ts +164 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/utils/utils.d.ts +19 -3
- package/dist/utils/utils.js +52 -3
- package/package.json +5 -3
- package/src/index.ts +10 -11
- package/src/messages/handlers.test.ts +10 -37
- package/src/messages/handlers.ts +2 -2
- package/src/plan/change-set.test.ts +220 -0
- package/src/plan/change-set.ts +225 -0
- package/src/plan/plan-types.ts +27 -0
- package/src/{entities → plan}/plan.test.ts +35 -29
- package/src/plan/plan.ts +353 -0
- package/src/{entities → plugin}/plugin.test.ts +14 -13
- package/src/{entities → plugin}/plugin.ts +28 -24
- package/src/resource/config-parser.ts +77 -0
- package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
- package/src/resource/parsed-resource-settings.ts +179 -0
- package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
- package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
- package/src/resource/resource-controller.ts +340 -0
- package/src/resource/resource-settings.test.ts +494 -0
- package/src/resource/resource-settings.ts +192 -0
- package/src/resource/resource.ts +149 -0
- package/src/resource/stateful-parameter.test.ts +93 -0
- package/src/resource/stateful-parameter.ts +217 -0
- package/src/utils/test-utils.test.ts +87 -0
- package/src/utils/utils.test.ts +2 -2
- package/src/utils/utils.ts +51 -5
- package/tsconfig.json +0 -1
- package/vitest.config.ts +10 -0
- package/src/entities/change-set.test.ts +0 -155
- package/src/entities/change-set.ts +0 -244
- package/src/entities/plan-types.ts +0 -44
- package/src/entities/plan.ts +0 -178
- package/src/entities/resource-options.ts +0 -155
- package/src/entities/resource-parameters.test.ts +0 -604
- package/src/entities/resource-types.ts +0 -31
- package/src/entities/resource.ts +0 -470
- package/src/entities/stateful-parameter.test.ts +0 -114
- package/src/entities/stateful-parameter.ts +0 -92
- package/src/entities/transform-parameter.ts +0 -13
- /package/src/{entities/errors.ts → errors.ts} +0 -0
package/dist/utils/utils.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import promiseSpawn from '@npmcli/promise-spawn';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
export var SpawnStatus;
|
|
3
4
|
(function (SpawnStatus) {
|
|
4
5
|
SpawnStatus["SUCCESS"] = "success";
|
|
5
6
|
SpawnStatus["ERROR"] = "error";
|
|
6
7
|
})(SpawnStatus || (SpawnStatus = {}));
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
11
|
+
* @param args Optional additional arguments to append
|
|
12
|
+
* @param opts Standard options for node spawn. Additional argument:
|
|
13
|
+
* throws determines if a shell will throw a JS error. Defaults to true
|
|
14
|
+
* @param extras From PromiseSpawn
|
|
15
|
+
*
|
|
16
|
+
* @see promiseSpawn
|
|
17
|
+
* @see spawn
|
|
18
|
+
*
|
|
19
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
20
|
+
*/
|
|
7
21
|
export async function codifySpawn(cmd, args, opts, extras) {
|
|
8
22
|
try {
|
|
23
|
+
// TODO: Need to benchmark the effects of using sh vs zsh for shell.
|
|
24
|
+
// Seems like zsh shells run slower
|
|
9
25
|
const result = await promiseSpawn(cmd, args ?? [], { ...opts, stdio: 'pipe', stdioString: true, shell: opts?.shell ?? process.env.SHELL }, extras);
|
|
10
26
|
if (isDebug()) {
|
|
11
27
|
console.log(`codifySpawn result for: ${cmd}`);
|
|
@@ -34,20 +50,53 @@ export async function codifySpawn(cmd, args, opts, extras) {
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
export function isDebug() {
|
|
37
|
-
return process.env.DEBUG != null && process.env.DEBUG.includes('codify');
|
|
53
|
+
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
38
54
|
}
|
|
39
55
|
export function splitUserConfig(config) {
|
|
40
|
-
const
|
|
56
|
+
const coreParameters = {
|
|
41
57
|
type: config.type,
|
|
42
58
|
...(config.name ? { name: config.name } : {}),
|
|
43
59
|
...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
|
|
44
60
|
};
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
62
|
const { type, name, dependsOn, ...parameters } = config;
|
|
46
63
|
return {
|
|
47
64
|
parameters: parameters,
|
|
48
|
-
|
|
65
|
+
coreParameters,
|
|
49
66
|
};
|
|
50
67
|
}
|
|
51
68
|
export function setsEqual(set1, set2) {
|
|
52
69
|
return set1.size === set2.size && [...set1].every((v) => set2.has(v));
|
|
53
70
|
}
|
|
71
|
+
const homeDirectory = os.homedir();
|
|
72
|
+
export function untildify(pathWithTilde) {
|
|
73
|
+
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
74
|
+
}
|
|
75
|
+
export function areArraysEqual(parameter, desired, current) {
|
|
76
|
+
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
77
|
+
throw new Error(`A non-array value:
|
|
78
|
+
|
|
79
|
+
Desired: ${JSON.stringify(desired, null, 2)}
|
|
80
|
+
|
|
81
|
+
Current: ${JSON.stringify(desired, null, 2)}
|
|
82
|
+
|
|
83
|
+
Was provided even though type array was specified.
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
if (desired.length !== current.length) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const desiredCopy = [...desired];
|
|
90
|
+
const currentCopy = [...current];
|
|
91
|
+
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
92
|
+
// an isElementEqual method. Time: O(n^2)
|
|
93
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
94
|
+
const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2));
|
|
95
|
+
if (idx === -1) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
desiredCopy.splice(counter, 1);
|
|
99
|
+
currentCopy.splice(idx, 1);
|
|
100
|
+
}
|
|
101
|
+
return currentCopy.length === 0;
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.0.77",
|
|
4
|
+
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"ajv": "^8.12.0",
|
|
16
16
|
"ajv-formats": "^2.1.1",
|
|
17
17
|
"codify-schemas": "1.0.45",
|
|
18
|
-
"@npmcli/promise-spawn": "^7.0.1"
|
|
18
|
+
"@npmcli/promise-spawn": "^7.0.1",
|
|
19
|
+
"uuid": "^10.0.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@oclif/prettier-config": "^0.2.1",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"@types/node": "^18",
|
|
25
26
|
"@types/semver": "^7.5.4",
|
|
26
27
|
"@types/sinon": "^17.0.3",
|
|
28
|
+
"@types/uuid": "^10.0.0",
|
|
27
29
|
"chai-as-promised": "^7.1.1",
|
|
28
30
|
"vitest": "^1.4.0",
|
|
29
31
|
"vitest-mock-extended": "^1.3.1",
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { Plugin } from './entities/plugin.js';
|
|
2
1
|
import { MessageHandler } from './messages/handlers.js';
|
|
2
|
+
import { Plugin } from './plugin/plugin.js';
|
|
3
3
|
|
|
4
|
-
export * from './
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
13
|
-
|
|
4
|
+
export * from './errors.js'
|
|
5
|
+
export * from './plan/change-set.js'
|
|
6
|
+
export * from './plan/plan.js'
|
|
7
|
+
export * from './plan/plan-types.js'
|
|
8
|
+
export * from './plugin/plugin.js'
|
|
9
|
+
export * from './resource/parsed-resource-settings.js';
|
|
10
|
+
export * from './resource/resource.js'
|
|
11
|
+
export * from './resource/resource-settings.js'
|
|
12
|
+
export * from './resource/stateful-parameter.js'
|
|
14
13
|
export * from './utils/utils.js'
|
|
15
14
|
|
|
16
15
|
export async function runPlugin(plugin: Plugin) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { MessageHandler } from './handlers.js';
|
|
2
|
-
import { Plugin } from '../
|
|
2
|
+
import { Plugin } from '../plugin/plugin.js';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { mock } from 'vitest-mock-extended'
|
|
5
|
-
import { Resource } from '../
|
|
6
|
-
import { Plan } from '../entities/plan.js';
|
|
5
|
+
import { Resource } from '../resource/resource.js';
|
|
7
6
|
import { MessageStatus, ResourceOperation } from 'codify-schemas';
|
|
7
|
+
import { TestResource } from '../utils/test-utils.test.js';
|
|
8
8
|
|
|
9
9
|
describe('Message handler tests', () => {
|
|
10
10
|
it('handles plan requests', async () => {
|
|
@@ -151,7 +151,7 @@ describe('Message handler tests', () => {
|
|
|
151
151
|
})
|
|
152
152
|
|
|
153
153
|
it('handles errors for plan', async () => {
|
|
154
|
-
const resource=
|
|
154
|
+
const resource = new TestResource()
|
|
155
155
|
const plugin = testPlugin(resource);
|
|
156
156
|
|
|
157
157
|
const handler = new MessageHandler(plugin);
|
|
@@ -179,7 +179,7 @@ describe('Message handler tests', () => {
|
|
|
179
179
|
})
|
|
180
180
|
|
|
181
181
|
it('handles errors for apply (create)', async () => {
|
|
182
|
-
const resource=
|
|
182
|
+
const resource = new TestResource()
|
|
183
183
|
const plugin = testPlugin(resource);
|
|
184
184
|
|
|
185
185
|
const handler = new MessageHandler(plugin);
|
|
@@ -188,7 +188,6 @@ describe('Message handler tests', () => {
|
|
|
188
188
|
expect(message).toMatchObject({
|
|
189
189
|
cmd: 'apply_Response',
|
|
190
190
|
status: MessageStatus.ERROR,
|
|
191
|
-
data: 'Create error',
|
|
192
191
|
})
|
|
193
192
|
return true;
|
|
194
193
|
}
|
|
@@ -206,7 +205,7 @@ describe('Message handler tests', () => {
|
|
|
206
205
|
})
|
|
207
206
|
|
|
208
207
|
it('handles errors for apply (destroy)', async () => {
|
|
209
|
-
const resource=
|
|
208
|
+
const resource = new TestResource()
|
|
210
209
|
const plugin = testPlugin(resource);
|
|
211
210
|
|
|
212
211
|
const handler = new MessageHandler(plugin);
|
|
@@ -215,7 +214,6 @@ describe('Message handler tests', () => {
|
|
|
215
214
|
expect(message).toMatchObject({
|
|
216
215
|
cmd: 'apply_Response',
|
|
217
216
|
status: MessageStatus.ERROR,
|
|
218
|
-
data: 'Destroy error',
|
|
219
217
|
})
|
|
220
218
|
return true;
|
|
221
219
|
}
|
|
@@ -231,33 +229,8 @@ describe('Message handler tests', () => {
|
|
|
231
229
|
}
|
|
232
230
|
})).rejects.to.not.throw;
|
|
233
231
|
})
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const testResource = () => new class extends Resource<any> {
|
|
237
|
-
constructor() {
|
|
238
|
-
super({ type: 'resourceA' });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async refresh(keys: Map<keyof any, any>): Promise<Partial<any> | null> {
|
|
242
|
-
throw new Error('Refresh error');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
applyCreate(plan: Plan<any>): Promise<void> {
|
|
246
|
-
throw new Error('Create error');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
applyDestroy(plan: Plan<any>): Promise<void> {
|
|
250
|
-
throw new Error('Destroy error');
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const testPlugin = (resource: Resource<any>) => new class extends Plugin {
|
|
255
|
-
constructor() {
|
|
256
|
-
const map = new Map();
|
|
257
|
-
map.set('resourceA', resource);
|
|
258
|
-
|
|
259
|
-
super('name', map);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
232
|
});
|
|
233
|
+
|
|
234
|
+
function testPlugin(resource: Resource<any>) {
|
|
235
|
+
return Plugin.create('plugin', [resource])
|
|
236
|
+
}
|
package/src/messages/handlers.ts
CHANGED
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
ValidateResponseDataSchema
|
|
16
16
|
} from 'codify-schemas';
|
|
17
17
|
|
|
18
|
-
import { SudoError } from '../
|
|
19
|
-
import { Plugin } from '../
|
|
18
|
+
import { SudoError } from '../errors.js';
|
|
19
|
+
import { Plugin } from '../plugin/plugin.js';
|
|
20
20
|
|
|
21
21
|
const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) => Promise<unknown>; requestValidator: SchemaObject; responseValidator: SchemaObject }> = {
|
|
22
22
|
'apply': {
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { ChangeSet } from './change-set.js';
|
|
2
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('Change set tests', () => {
|
|
6
|
+
it ('Correctly diffs two resource configs (modify)', () => {
|
|
7
|
+
const after = {
|
|
8
|
+
propA: 'before',
|
|
9
|
+
propB: 'before'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const before = {
|
|
13
|
+
propA: 'after',
|
|
14
|
+
propB: 'after'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
18
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
19
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
20
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.MODIFY);
|
|
21
|
+
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it ('Correctly diffs two resource configs (add)', () => {
|
|
25
|
+
const after = {
|
|
26
|
+
propA: 'before',
|
|
27
|
+
propB: 'after'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const before = {
|
|
31
|
+
propA: 'after',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cs = ChangeSet.calculateModification(after, before,);
|
|
35
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
36
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
37
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.ADD);
|
|
38
|
+
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
39
|
+
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it ('Correctly diffs two resource configs (remove)', () => {
|
|
43
|
+
const after = {
|
|
44
|
+
propA: 'after',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const before = {
|
|
48
|
+
propA: 'before',
|
|
49
|
+
propB: 'before'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
53
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
54
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
55
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
56
|
+
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it ('Correctly diffs two resource configs (no-op)', () => {
|
|
60
|
+
const after = {
|
|
61
|
+
propA: 'prop',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const before = {
|
|
65
|
+
propA: 'prop',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cs = ChangeSet.calculateModification(after, before);
|
|
69
|
+
expect(cs.parameterChanges.length).to.eq(1);
|
|
70
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
71
|
+
expect(cs.operation).to.eq(ResourceOperation.NOOP)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('Correctly diffs two resource configs (create)', () => {
|
|
75
|
+
const cs = ChangeSet.create({
|
|
76
|
+
propA: 'prop',
|
|
77
|
+
propB: 'propB'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
81
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.ADD);
|
|
82
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.ADD);
|
|
83
|
+
expect(cs.operation).to.eq(ResourceOperation.CREATE)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('Correctly diffs two resource configs (destory)', () => {
|
|
87
|
+
const cs = ChangeSet.destroy({
|
|
88
|
+
propA: 'prop',
|
|
89
|
+
propB: 'propB'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
93
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.REMOVE);
|
|
94
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
95
|
+
expect(cs.operation).to.eq(ResourceOperation.DESTROY)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it ('handles simple arrays', () => {
|
|
99
|
+
const before = {
|
|
100
|
+
propA: ['a', 'b', 'c'],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const after = {
|
|
104
|
+
propA: ['b', 'a', 'c'],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cs = ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
|
|
108
|
+
expect(cs.parameterChanges.length).to.eq(1);
|
|
109
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
110
|
+
expect(cs.operation).to.eq(ResourceOperation.NOOP)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('handles simple arrays 2', () => {
|
|
114
|
+
const after = {
|
|
115
|
+
propA: ['a', 'b', 'c'],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const before = {
|
|
119
|
+
propA: ['b', 'a'],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cs = ChangeSet.calculateModification(after, before, { propA: { type: 'array' } });
|
|
123
|
+
expect(cs.parameterChanges.length).to.eq(1);
|
|
124
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
125
|
+
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('determines the order of operations with canModify 1', () => {
|
|
129
|
+
const after = {
|
|
130
|
+
propA: 'after',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const before = {
|
|
134
|
+
propA: 'before',
|
|
135
|
+
propB: 'before'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const cs = ChangeSet.calculateModification(after, before, { propA: { canModify: true } });
|
|
139
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
140
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
141
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
142
|
+
expect(cs.operation).to.eq(ResourceOperation.RECREATE)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('determines the order of operations with canModify 2', () => {
|
|
146
|
+
const after = {
|
|
147
|
+
propA: 'after',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const before = {
|
|
151
|
+
propA: 'before',
|
|
152
|
+
propB: 'before'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const cs = ChangeSet.calculateModification<any>(after, before, {
|
|
156
|
+
propA: { canModify: true },
|
|
157
|
+
propB: { canModify: true }
|
|
158
|
+
});
|
|
159
|
+
expect(cs.parameterChanges.length).to.eq(2);
|
|
160
|
+
expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
161
|
+
expect(cs.parameterChanges[1].operation).to.eq(ParameterOperation.REMOVE);
|
|
162
|
+
expect(cs.operation).to.eq(ResourceOperation.MODIFY)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
it('correctly determines array equality', () => {
|
|
167
|
+
const arrA = ['a', 'b', 'd'];
|
|
168
|
+
const arrB = ['a', 'b', 'd'];
|
|
169
|
+
|
|
170
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
171
|
+
|
|
172
|
+
expect(result.operation).to.eq(ResourceOperation.NOOP);
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('correctly determines array equality 2', () => {
|
|
176
|
+
const arrA = ['a', 'b'];
|
|
177
|
+
const arrB = ['a', 'b', 'd'];
|
|
178
|
+
|
|
179
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
180
|
+
|
|
181
|
+
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('correctly determines array equality 3', () => {
|
|
185
|
+
const arrA = ['b', 'a', 'd'];
|
|
186
|
+
const arrB = ['a', 'b', 'd'];
|
|
187
|
+
|
|
188
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, { propA: { type: 'array' } })
|
|
189
|
+
|
|
190
|
+
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('correctly determines array equality 4', () => {
|
|
194
|
+
const arrA = [{ key1: 'a' }, { key1: 'a' }, { key1: 'a' }];
|
|
195
|
+
const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
|
|
196
|
+
|
|
197
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
|
|
198
|
+
propA: {
|
|
199
|
+
type: 'array',
|
|
200
|
+
isElementEqual: (a, b) => a.key1 === b.key1
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('correctly determines array equality 5', () => {
|
|
208
|
+
const arrA = [{ key1: 'b' }, { key1: 'a' }, { key1: 'a' }];
|
|
209
|
+
const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
|
|
210
|
+
|
|
211
|
+
const result = ChangeSet.calculateModification({ propA: arrA }, { propA: arrB }, {
|
|
212
|
+
propA: {
|
|
213
|
+
type: 'array',
|
|
214
|
+
isElementEqual: (a, b) => a.key1 === b.key1
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.NOOP);
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
|
|
3
|
+
import { ArrayParameterSetting, ParameterSetting, StatefulParameterSetting } from '../resource/resource-settings.js';
|
|
4
|
+
import { areArraysEqual } from '../utils/utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A parameter change describes a parameter level change to a resource.
|
|
8
|
+
*/
|
|
9
|
+
export interface ParameterChange<T extends StringIndexedObject> {
|
|
10
|
+
/**
|
|
11
|
+
* The name of the parameter
|
|
12
|
+
*/
|
|
13
|
+
name: keyof T & string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The operation to be performed on the parameter.
|
|
17
|
+
*/
|
|
18
|
+
operation: ParameterOperation;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The previous value of the resource (the current value on the system)
|
|
22
|
+
*/
|
|
23
|
+
previousValue: any | null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The new value of the resource (the desired value)
|
|
27
|
+
*/
|
|
28
|
+
newValue: any | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Change set will coerce undefined values to null because undefined is not valid JSON
|
|
32
|
+
export class ChangeSet<T extends StringIndexedObject> {
|
|
33
|
+
operation: ResourceOperation
|
|
34
|
+
parameterChanges: Array<ParameterChange<T>>
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
operation: ResourceOperation,
|
|
38
|
+
parameterChanges: Array<ParameterChange<T>>
|
|
39
|
+
) {
|
|
40
|
+
this.operation = operation;
|
|
41
|
+
this.parameterChanges = parameterChanges;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get desiredParameters(): T {
|
|
45
|
+
return this.parameterChanges
|
|
46
|
+
.reduce((obj, pc) => ({
|
|
47
|
+
...obj,
|
|
48
|
+
[pc.name]: pc.newValue,
|
|
49
|
+
}), {}) as T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get currentParameters(): T {
|
|
53
|
+
return this.parameterChanges
|
|
54
|
+
.reduce((obj, pc) => ({
|
|
55
|
+
...obj,
|
|
56
|
+
[pc.name]: pc.previousValue,
|
|
57
|
+
}), {}) as T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static empty<T extends StringIndexedObject>(): ChangeSet<T> {
|
|
61
|
+
return new ChangeSet<T>(ResourceOperation.NOOP, []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T> {
|
|
65
|
+
const parameterChanges = Object.entries(desired)
|
|
66
|
+
.map(([k, v]) => ({
|
|
67
|
+
name: k,
|
|
68
|
+
operation: ParameterOperation.ADD,
|
|
69
|
+
previousValue: null,
|
|
70
|
+
newValue: v ?? null,
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
return new ChangeSet(ResourceOperation.CREATE, parameterChanges);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T> {
|
|
77
|
+
const parameterChanges = Object.entries(current)
|
|
78
|
+
.map(([k, v]) => ({
|
|
79
|
+
name: k,
|
|
80
|
+
operation: ParameterOperation.REMOVE,
|
|
81
|
+
previousValue: v ?? null,
|
|
82
|
+
newValue: null,
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static calculateModification<T extends StringIndexedObject>(
|
|
89
|
+
desired: Partial<T>,
|
|
90
|
+
current: Partial<T>,
|
|
91
|
+
parameterSettings: Partial<Record<keyof T, ParameterSetting>> = {},
|
|
92
|
+
): ChangeSet<T> {
|
|
93
|
+
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
94
|
+
|
|
95
|
+
const statefulParameterKeys = new Set(
|
|
96
|
+
Object.entries(parameterSettings)
|
|
97
|
+
.filter(([, v]) => v?.type === 'stateful')
|
|
98
|
+
.map(([k]) => k)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const resourceOperation = pc
|
|
102
|
+
.filter((change) => change.operation !== ParameterOperation.NOOP)
|
|
103
|
+
.reduce((operation: ResourceOperation, curr: ParameterChange<T>) => {
|
|
104
|
+
let newOperation: ResourceOperation;
|
|
105
|
+
if (statefulParameterKeys.has(curr.name)) {
|
|
106
|
+
newOperation = ResourceOperation.MODIFY // All stateful parameters are modify only
|
|
107
|
+
} else if (parameterSettings[curr.name]?.canModify) {
|
|
108
|
+
newOperation = ResourceOperation.MODIFY
|
|
109
|
+
} else {
|
|
110
|
+
newOperation = ResourceOperation.RECREATE; // Default to Re-create. Should handle the majority of use cases
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return ChangeSet.combineResourceOperations(operation, newOperation);
|
|
114
|
+
}, ResourceOperation.NOOP);
|
|
115
|
+
|
|
116
|
+
return new ChangeSet<T>(resourceOperation, pc);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static calculateParameterChanges<T extends StringIndexedObject>(
|
|
120
|
+
desiredParameters: Partial<T>,
|
|
121
|
+
currentParameters: Partial<T>,
|
|
122
|
+
parameterOptions?: Partial<Record<keyof T, ParameterSetting>>,
|
|
123
|
+
): ParameterChange<T>[] {
|
|
124
|
+
const parameterChangeSet = new Array<ParameterChange<T>>();
|
|
125
|
+
|
|
126
|
+
// Filter out null and undefined values or else the diff below will not work
|
|
127
|
+
const desired = Object.fromEntries(
|
|
128
|
+
Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined)
|
|
129
|
+
) as Partial<T>
|
|
130
|
+
|
|
131
|
+
const current = Object.fromEntries(
|
|
132
|
+
Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined)
|
|
133
|
+
) as Partial<T>
|
|
134
|
+
|
|
135
|
+
for (const [k, v] of Object.entries(current)) {
|
|
136
|
+
if (desired?.[k] === null || desired?.[k] === undefined) {
|
|
137
|
+
parameterChangeSet.push({
|
|
138
|
+
name: k,
|
|
139
|
+
previousValue: v ?? null,
|
|
140
|
+
newValue: null,
|
|
141
|
+
operation: ParameterOperation.REMOVE,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
delete current[k];
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
149
|
+
parameterChangeSet.push({
|
|
150
|
+
name: k,
|
|
151
|
+
previousValue: v ?? null,
|
|
152
|
+
newValue: desired[k] ?? null,
|
|
153
|
+
operation: ParameterOperation.MODIFY,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
delete current[k];
|
|
157
|
+
delete desired[k];
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
parameterChangeSet.push({
|
|
162
|
+
name: k,
|
|
163
|
+
previousValue: v ?? null,
|
|
164
|
+
newValue: desired[k] ?? null,
|
|
165
|
+
operation: ParameterOperation.NOOP,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
delete current[k];
|
|
169
|
+
delete desired[k];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (Object.keys(current).length > 0) {
|
|
173
|
+
throw new Error('Diff algorithm error');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const [k, v] of Object.entries(desired)) {
|
|
177
|
+
parameterChangeSet.push({
|
|
178
|
+
name: k,
|
|
179
|
+
previousValue: null,
|
|
180
|
+
newValue: v ?? null,
|
|
181
|
+
operation: ParameterOperation.ADD,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return parameterChangeSet;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private static combineResourceOperations(prev: ResourceOperation, next: ResourceOperation) {
|
|
189
|
+
const orderOfOperations = [
|
|
190
|
+
ResourceOperation.NOOP,
|
|
191
|
+
ResourceOperation.MODIFY,
|
|
192
|
+
ResourceOperation.RECREATE,
|
|
193
|
+
ResourceOperation.CREATE,
|
|
194
|
+
ResourceOperation.DESTROY,
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
const indexPrev = orderOfOperations.indexOf(prev);
|
|
198
|
+
const indexNext = orderOfOperations.indexOf(next);
|
|
199
|
+
|
|
200
|
+
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static isSame(
|
|
204
|
+
desired: unknown,
|
|
205
|
+
current: unknown,
|
|
206
|
+
setting?: ParameterSetting,
|
|
207
|
+
): boolean {
|
|
208
|
+
switch (setting?.type) {
|
|
209
|
+
case 'stateful': {
|
|
210
|
+
const statefulSetting = (setting as StatefulParameterSetting).definition.getSettings()
|
|
211
|
+
|
|
212
|
+
return ChangeSet.isSame(desired, current, statefulSetting as ParameterSetting);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case 'array': {
|
|
216
|
+
const arrayParameter = setting as ArrayParameterSetting;
|
|
217
|
+
return areArraysEqual(arrayParameter, desired, current)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
default: {
|
|
221
|
+
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|