codify-plugin-lib 1.0.76 → 1.0.78

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.
Files changed (74) hide show
  1. package/.eslintrc.json +11 -4
  2. package/.github/workflows/release.yaml +19 -0
  3. package/.github/workflows/unit-test-ci.yaml +19 -0
  4. package/dist/errors.d.ts +4 -0
  5. package/dist/errors.js +7 -0
  6. package/dist/index.d.ts +10 -10
  7. package/dist/index.js +9 -9
  8. package/dist/messages/handlers.d.ts +1 -1
  9. package/dist/messages/handlers.js +2 -1
  10. package/dist/plan/change-set.d.ts +47 -0
  11. package/dist/plan/change-set.js +156 -0
  12. package/dist/plan/plan-types.d.ts +23 -0
  13. package/dist/plan/plan-types.js +1 -0
  14. package/dist/plan/plan.d.ts +59 -0
  15. package/dist/plan/plan.js +228 -0
  16. package/dist/plugin/plugin.d.ts +17 -0
  17. package/dist/plugin/plugin.js +83 -0
  18. package/dist/resource/config-parser.d.ts +14 -0
  19. package/dist/resource/config-parser.js +48 -0
  20. package/dist/resource/parsed-resource-settings.d.ts +26 -0
  21. package/dist/resource/parsed-resource-settings.js +126 -0
  22. package/dist/resource/resource-controller.d.ts +30 -0
  23. package/dist/resource/resource-controller.js +249 -0
  24. package/dist/resource/resource-settings.d.ts +149 -0
  25. package/dist/resource/resource-settings.js +9 -0
  26. package/dist/resource/resource.d.ts +137 -0
  27. package/dist/resource/resource.js +44 -0
  28. package/dist/resource/stateful-parameter.d.ts +164 -0
  29. package/dist/resource/stateful-parameter.js +94 -0
  30. package/dist/utils/spawn-2.d.ts +5 -0
  31. package/dist/utils/spawn-2.js +7 -0
  32. package/dist/utils/spawn.d.ts +29 -0
  33. package/dist/utils/spawn.js +124 -0
  34. package/dist/utils/utils.d.ts +19 -3
  35. package/dist/utils/utils.js +52 -3
  36. package/package.json +5 -3
  37. package/src/index.ts +10 -11
  38. package/src/messages/handlers.test.ts +10 -37
  39. package/src/messages/handlers.ts +2 -2
  40. package/src/plan/change-set.test.ts +220 -0
  41. package/src/plan/change-set.ts +235 -0
  42. package/src/plan/plan-types.ts +27 -0
  43. package/src/{entities → plan}/plan.test.ts +35 -29
  44. package/src/plan/plan.ts +353 -0
  45. package/src/{entities → plugin}/plugin.test.ts +14 -13
  46. package/src/{entities → plugin}/plugin.ts +28 -24
  47. package/src/resource/config-parser.ts +77 -0
  48. package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
  49. package/src/resource/parsed-resource-settings.ts +179 -0
  50. package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
  51. package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
  52. package/src/resource/resource-controller.ts +343 -0
  53. package/src/resource/resource-settings.test.ts +494 -0
  54. package/src/resource/resource-settings.ts +192 -0
  55. package/src/resource/resource.ts +149 -0
  56. package/src/resource/stateful-parameter.test.ts +93 -0
  57. package/src/resource/stateful-parameter.ts +217 -0
  58. package/src/utils/test-utils.test.ts +87 -0
  59. package/src/utils/utils.test.ts +2 -2
  60. package/src/utils/utils.ts +51 -5
  61. package/tsconfig.json +0 -1
  62. package/vitest.config.ts +10 -0
  63. package/src/entities/change-set.test.ts +0 -155
  64. package/src/entities/change-set.ts +0 -244
  65. package/src/entities/plan-types.ts +0 -44
  66. package/src/entities/plan.ts +0 -178
  67. package/src/entities/resource-options.ts +0 -155
  68. package/src/entities/resource-parameters.test.ts +0 -604
  69. package/src/entities/resource-types.ts +0 -31
  70. package/src/entities/resource.ts +0 -470
  71. package/src/entities/stateful-parameter.test.ts +0 -114
  72. package/src/entities/stateful-parameter.ts +0 -92
  73. package/src/entities/transform-parameter.ts +0 -13
  74. /package/src/{entities/errors.ts → errors.ts} +0 -0
@@ -0,0 +1,235 @@
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
+ /**
120
+ * Calculates the differences between the desired and current parameters,
121
+ * and returns a list of parameter changes that describe what needs to be added,
122
+ * removed, or modified to match the desired state.
123
+ *
124
+ * @param {Partial<T>} desiredParameters - The desired target state of the parameters.
125
+ * @param {Partial<T>} currentParameters - The current state of the parameters.
126
+ * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
127
+ * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
128
+ */
129
+ private static calculateParameterChanges<T extends StringIndexedObject>(
130
+ desiredParameters: Partial<T>,
131
+ currentParameters: Partial<T>,
132
+ parameterOptions?: Partial<Record<keyof T, ParameterSetting>>,
133
+ ): ParameterChange<T>[] {
134
+ const parameterChangeSet = new Array<ParameterChange<T>>();
135
+
136
+ // Filter out null and undefined values or else the diff below will not work
137
+ const desired = Object.fromEntries(
138
+ Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined)
139
+ ) as Partial<T>
140
+
141
+ const current = Object.fromEntries(
142
+ Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined)
143
+ ) as Partial<T>
144
+
145
+ for (const [k, v] of Object.entries(current)) {
146
+ if (desired?.[k] === null || desired?.[k] === undefined) {
147
+ parameterChangeSet.push({
148
+ name: k,
149
+ previousValue: v ?? null,
150
+ newValue: null,
151
+ operation: ParameterOperation.REMOVE,
152
+ })
153
+
154
+ delete current[k];
155
+ continue;
156
+ }
157
+
158
+ if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
159
+ parameterChangeSet.push({
160
+ name: k,
161
+ previousValue: v ?? null,
162
+ newValue: desired[k] ?? null,
163
+ operation: ParameterOperation.MODIFY,
164
+ })
165
+
166
+ delete current[k];
167
+ delete desired[k];
168
+ continue;
169
+ }
170
+
171
+ parameterChangeSet.push({
172
+ name: k,
173
+ previousValue: v ?? null,
174
+ newValue: desired[k] ?? null,
175
+ operation: ParameterOperation.NOOP,
176
+ })
177
+
178
+ delete current[k];
179
+ delete desired[k];
180
+ }
181
+
182
+ if (Object.keys(current).length > 0) {
183
+ throw new Error('Diff algorithm error');
184
+ }
185
+
186
+ for (const [k, v] of Object.entries(desired)) {
187
+ parameterChangeSet.push({
188
+ name: k,
189
+ previousValue: null,
190
+ newValue: v ?? null,
191
+ operation: ParameterOperation.ADD,
192
+ })
193
+ }
194
+
195
+ return parameterChangeSet;
196
+ }
197
+
198
+ private static combineResourceOperations(prev: ResourceOperation, next: ResourceOperation) {
199
+ const orderOfOperations = [
200
+ ResourceOperation.NOOP,
201
+ ResourceOperation.MODIFY,
202
+ ResourceOperation.RECREATE,
203
+ ResourceOperation.CREATE,
204
+ ResourceOperation.DESTROY,
205
+ ]
206
+
207
+ const indexPrev = orderOfOperations.indexOf(prev);
208
+ const indexNext = orderOfOperations.indexOf(next);
209
+
210
+ return orderOfOperations[Math.max(indexPrev, indexNext)];
211
+ }
212
+
213
+ private static isSame(
214
+ desired: unknown,
215
+ current: unknown,
216
+ setting?: ParameterSetting,
217
+ ): boolean {
218
+ switch (setting?.type) {
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
+ }
234
+ }
235
+ }
@@ -0,0 +1,27 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+
3
+ import { Plan } from './plan.js';
4
+
5
+ /**
6
+ * A narrower type for plans for CREATE operations. Only desiredConfig is not null.
7
+ */
8
+ export interface CreatePlan<T extends StringIndexedObject> extends Plan<T> {
9
+ desiredConfig: T;
10
+ currentConfig: null;
11
+ }
12
+
13
+ /**
14
+ * A narrower type for plans for DESTROY operations. Only currentConfig is not null.
15
+ */
16
+ export interface DestroyPlan<T extends StringIndexedObject> extends Plan<T> {
17
+ desiredConfig: null;
18
+ currentConfig: T;
19
+ }
20
+
21
+ /**
22
+ * A narrower type for plans for MODIFY and RE-CREATE operations.
23
+ */
24
+ export interface ModifyPlan<T extends StringIndexedObject> extends Plan<T> {
25
+ desiredConfig: T;
26
+ currentConfig: T;
27
+ }
@@ -1,12 +1,15 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Plan } from './plan.js';
3
- import { TestResource } from './resource.test.js';
4
3
  import { ParameterOperation, ResourceOperation } from 'codify-schemas';
5
- import { Resource } from './resource.js';
4
+ import { TestConfig, TestResource } from '../utils/test-utils.test.js';
5
+ import { ResourceController } from '../resource/resource-controller.js';
6
+ import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
7
+ import { ResourceSettings } from '../resource/resource-settings.js';
6
8
 
7
9
  describe('Plan entity tests', () => {
8
10
  it('Adds default values properly when plan is parsed from request (Create)', () => {
9
- const resource = createResource();
11
+ const resource = createTestResource()
12
+ const controller = new ResourceController(resource);
10
13
 
11
14
  const plan = Plan.fromResponse({
12
15
  operation: ResourceOperation.CREATE,
@@ -17,7 +20,7 @@ describe('Plan entity tests', () => {
17
20
  previousValue: null,
18
21
  newValue: 'propBValue'
19
22
  }]
20
- }, resource.defaultValues);
23
+ }, controller.parsedSettings.defaultValues);
21
24
 
22
25
  expect(plan.currentConfig).to.be.null;
23
26
 
@@ -33,7 +36,8 @@ describe('Plan entity tests', () => {
33
36
  })
34
37
 
35
38
  it('Adds default values properly when plan is parsed from request (Destroy)', () => {
36
- const resource = createResource();
39
+ const resource = createTestResource()
40
+ const controller = new ResourceController(resource);
37
41
 
38
42
  const plan = Plan.fromResponse({
39
43
  operation: ResourceOperation.DESTROY,
@@ -44,7 +48,7 @@ describe('Plan entity tests', () => {
44
48
  previousValue: 'propBValue',
45
49
  newValue: null,
46
50
  }]
47
- }, resource.defaultValues);
51
+ }, controller.parsedSettings.defaultValues);
48
52
 
49
53
  expect(plan.currentConfig).toMatchObject({
50
54
  type: 'type',
@@ -60,7 +64,8 @@ describe('Plan entity tests', () => {
60
64
  })
61
65
 
62
66
  it('Adds default values properly when plan is parsed from request (No-op)', () => {
63
- const resource = createResource();
67
+ const resource = createTestResource()
68
+ const controller = new ResourceController(resource);
64
69
 
65
70
  const plan = Plan.fromResponse({
66
71
  operation: ResourceOperation.NOOP,
@@ -71,7 +76,7 @@ describe('Plan entity tests', () => {
71
76
  previousValue: 'propBValue',
72
77
  newValue: 'propBValue',
73
78
  }]
74
- }, resource.defaultValues);
79
+ }, controller.parsedSettings.defaultValues);
75
80
 
76
81
  expect(plan.currentConfig).toMatchObject({
77
82
  type: 'type',
@@ -91,7 +96,8 @@ describe('Plan entity tests', () => {
91
96
  })
92
97
 
93
98
  it('Does not add default value if a value has already been specified', () => {
94
- const resource = createResource();
99
+ const resource = createTestResource()
100
+ const controller = new ResourceController(resource);
95
101
 
96
102
  const plan = Plan.fromResponse({
97
103
  operation: ResourceOperation.CREATE,
@@ -107,7 +113,7 @@ describe('Plan entity tests', () => {
107
113
  previousValue: null,
108
114
  newValue: 'propAValue',
109
115
  }]
110
- }, resource.defaultValues);
116
+ }, controller.parsedSettings.defaultValues);
111
117
 
112
118
  expect(plan.currentConfig).to.be.null
113
119
 
@@ -123,19 +129,17 @@ describe('Plan entity tests', () => {
123
129
  })
124
130
 
125
131
  it('Returns the original resource names', () => {
126
- const resource = createResource();
127
-
128
- const plan = Plan.create(
129
- {
130
- propA: 'propA',
131
- },
132
- {
133
- propA: 'propA2',
134
- },
135
- {
132
+ const plan = Plan.calculate<TestConfig>({
133
+ desiredParameters: { propA: 'propA' },
134
+ currentParametersArray: [{ propA: 'propA2' }],
135
+ stateParameters: null,
136
+ coreParameters: {
136
137
  type: 'type',
137
138
  name: 'name1'
138
- }, { statefulMode: false });
139
+ },
140
+ settings: new ParsedResourceSettings<TestConfig>({ id: 'type' }),
141
+ statefulMode: false,
142
+ });
139
143
 
140
144
  expect(plan.toResponse()).toMatchObject({
141
145
  resourceType: 'type',
@@ -145,15 +149,17 @@ describe('Plan entity tests', () => {
145
149
  })
146
150
  })
147
151
 
148
- function createResource(): Resource<any> {
152
+ function createTestResource() {
149
153
  return new class extends TestResource {
150
- constructor() {
151
- super({
152
- type: 'type',
153
- parameterOptions: {
154
- propA: { default: 'defaultA' }
154
+ getSettings(): ResourceSettings<TestConfig> {
155
+ return {
156
+ id: 'type',
157
+ parameterSettings: {
158
+ propA: {
159
+ default: 'defaultA'
160
+ }
155
161
  }
156
- });
162
+ }
157
163
  }
158
- }
164
+ };
159
165
  }