codify-plugin-lib 1.0.42 → 1.0.43

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.
@@ -12,7 +12,6 @@ export declare class ChangeSet<T extends StringIndexedObject> {
12
12
  constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
13
13
  get desiredParameters(): T;
14
14
  get currentParameters(): T;
15
- static newCreate<T extends {}>(desiredConfig: T): ChangeSet<StringIndexedObject>;
16
15
  static calculateParameterChangeSet<T extends StringIndexedObject>(desired: T | null, current: T | null, options: {
17
16
  statefulMode: boolean;
18
17
  parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
@@ -20,19 +20,6 @@ export class ChangeSet {
20
20
  [pc.name]: pc.previousValue,
21
21
  }), {});
22
22
  }
23
- static newCreate(desiredConfig) {
24
- const parameterChangeSet = Object.entries(desiredConfig)
25
- .filter(([k,]) => k !== 'type' && k !== 'name')
26
- .map(([k, v]) => {
27
- return {
28
- name: k,
29
- operation: ParameterOperation.ADD,
30
- previousValue: null,
31
- newValue: v,
32
- };
33
- });
34
- return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
35
- }
36
23
  static calculateParameterChangeSet(desired, current, options) {
37
24
  if (options.statefulMode) {
38
25
  return ChangeSet.calculateStatefulModeChangeSet(desired, current, options.parameterConfigurations);
@@ -6,7 +6,7 @@ export declare class Plan<T extends StringIndexedObject> {
6
6
  changeSet: ChangeSet<T>;
7
7
  resourceMetadata: ResourceConfig;
8
8
  constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
9
- static create<T extends StringIndexedObject>(desiredConfig: Partial<T> & ResourceConfig, currentConfig: Partial<T> & ResourceConfig | null, configuration: PlanConfiguration<T>): Plan<T>;
9
+ static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, configuration: PlanConfiguration<T>): Plan<T>;
10
10
  getResourceType(): string;
11
11
  static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
12
12
  get desiredConfig(): T;
@@ -1,7 +1,6 @@
1
1
  import { ChangeSet } from './change-set.js';
2
2
  import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
3
3
  import { randomUUID } from 'crypto';
4
- import { splitUserConfig } from '../utils/utils.js';
5
4
  export class Plan {
6
5
  id;
7
6
  changeSet;
@@ -11,19 +10,17 @@ export class Plan {
11
10
  this.changeSet = changeSet;
12
11
  this.resourceMetadata = resourceMetadata;
13
12
  }
14
- static create(desiredConfig, currentConfig, configuration) {
13
+ static create(desiredParameters, currentParameters, resourceMetadata, configuration) {
15
14
  const parameterConfigurations = configuration.parameterConfigurations ?? {};
16
15
  const statefulParameterNames = new Set([...Object.entries(parameterConfigurations)]
17
16
  .filter(([k, v]) => v.isStatefulParameter)
18
17
  .map(([k, v]) => k));
19
- const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
20
- const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
21
18
  const parameterChangeSet = ChangeSet.calculateParameterChangeSet(desiredParameters, currentParameters, { statefulMode: configuration.statefulMode, parameterConfigurations });
22
19
  let resourceOperation;
23
- if (!currentConfig && desiredConfig) {
20
+ if (!currentParameters && desiredParameters) {
24
21
  resourceOperation = ResourceOperation.CREATE;
25
22
  }
26
- else if (currentConfig && !desiredConfig) {
23
+ else if (currentParameters && !desiredParameters) {
27
24
  resourceOperation = ResourceOperation.DESTROY;
28
25
  }
29
26
  else {
@@ -33,18 +33,18 @@ export class Resource {
33
33
  const keysToRefresh = new Set(Object.keys(resourceParameters));
34
34
  const currentParameters = await this.refresh(keysToRefresh);
35
35
  if (currentParameters == null) {
36
- return Plan.create(desiredConfig, null, planConfiguration);
36
+ return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
37
37
  }
38
38
  this.validateRefreshResults(currentParameters, keysToRefresh);
39
39
  for (const statefulParameter of statefulParameters) {
40
40
  const desiredValue = desiredParameters[statefulParameter.name];
41
- let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
41
+ let currentValue = await statefulParameter.refresh() ?? undefined;
42
42
  if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
43
43
  currentValue = currentValue.filter((p) => desiredValue?.includes(p));
44
44
  }
45
45
  currentParameters[statefulParameter.name] = currentValue;
46
46
  }
47
- return Plan.create(desiredConfig, { ...currentParameters, ...resourceMetadata }, planConfiguration);
47
+ return Plan.create(desiredParameters, currentParameters, resourceMetadata, planConfiguration);
48
48
  }
49
49
  async apply(plan) {
50
50
  if (plan.getResourceType() !== this.typeId) {
@@ -12,17 +12,18 @@ export declare abstract class StatefulParameter<T extends StringIndexedObject, V
12
12
  readonly name: keyof T;
13
13
  readonly configuration: StatefulParameterConfiguration<T>;
14
14
  protected constructor(configuration: StatefulParameterConfiguration<T>);
15
- abstract refresh(previousValue: V | null): Promise<V | null>;
15
+ abstract refresh(): Promise<V | null>;
16
16
  abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
17
17
  abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
18
18
  abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
19
19
  }
20
20
  export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
21
+ configuration: ArrayStatefulParameterConfiguration<T>;
21
22
  constructor(configuration: ArrayStatefulParameterConfiguration<T>);
22
23
  applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
23
24
  applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
24
25
  applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
25
- abstract refresh(previousValue: V[] | null): Promise<V[] | null>;
26
+ abstract refresh(): Promise<V[] | null>;
26
27
  abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
27
28
  abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
28
29
  }
@@ -7,8 +7,10 @@ export class StatefulParameter {
7
7
  }
8
8
  }
9
9
  export class ArrayStatefulParameter extends StatefulParameter {
10
+ configuration;
10
11
  constructor(configuration) {
11
12
  super(configuration);
13
+ this.configuration = configuration;
12
14
  }
13
15
  async applyAdd(valuesToAdd, plan) {
14
16
  for (const value of valuesToAdd) {
@@ -16,8 +18,19 @@ export class ArrayStatefulParameter extends StatefulParameter {
16
18
  }
17
19
  }
18
20
  async applyModify(newValues, previousValues, allowDeletes, plan) {
19
- const valuesToAdd = newValues.filter((n) => !previousValues.includes(n));
20
- const valuesToRemove = previousValues.filter((n) => !newValues.includes(n));
21
+ const configuration = this.configuration;
22
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
23
+ if ((configuration).isElementEqual) {
24
+ return configuration.isElementEqual(n, p);
25
+ }
26
+ return n === p;
27
+ }));
28
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
29
+ if ((configuration).isElementEqual) {
30
+ return configuration.isElementEqual(n, p);
31
+ }
32
+ return n === p;
33
+ }));
21
34
  for (const value of valuesToAdd) {
22
35
  await this.applyAddItem(value, plan);
23
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -43,20 +43,20 @@ export class ChangeSet<T extends StringIndexedObject> {
43
43
  // const operation = ChangeSet.combineResourceOperations(prev, );
44
44
  // }
45
45
 
46
- static newCreate<T extends {}>(desiredConfig: T) {
47
- const parameterChangeSet = Object.entries(desiredConfig)
48
- .filter(([k,]) => k !== 'type' && k !== 'name')
49
- .map(([k, v]) => {
50
- return {
51
- name: k,
52
- operation: ParameterOperation.ADD,
53
- previousValue: null,
54
- newValue: v,
55
- }
56
- })
57
-
58
- return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
59
- }
46
+ // static newCreate<T extends {}>(desiredConfig: T) {
47
+ // const parameterChangeSet = Object.entries(desiredConfig)
48
+ // .filter(([k,]) => k !== 'type' && k !== 'name')
49
+ // .map(([k, v]) => {
50
+ // return {
51
+ // name: k,
52
+ // operation: ParameterOperation.ADD,
53
+ // previousValue: null,
54
+ // newValue: v,
55
+ // }
56
+ // })
57
+ //
58
+ // return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
59
+ // }
60
60
 
61
61
  static calculateParameterChangeSet<T extends StringIndexedObject>(
62
62
  desired: T | null,
@@ -9,7 +9,6 @@ import {
9
9
  } from 'codify-schemas';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
12
- import { splitUserConfig } from '../utils/utils.js';
13
12
 
14
13
  export class Plan<T extends StringIndexedObject> {
15
14
  id: string;
@@ -23,8 +22,9 @@ export class Plan<T extends StringIndexedObject> {
23
22
  }
24
23
 
25
24
  static create<T extends StringIndexedObject>(
26
- desiredConfig: Partial<T> & ResourceConfig,
27
- currentConfig: Partial<T> & ResourceConfig | null,
25
+ desiredParameters: Partial<T> | null,
26
+ currentParameters: Partial<T> | null,
27
+ resourceMetadata: ResourceConfig,
28
28
  configuration: PlanConfiguration<T>
29
29
  ): Plan<T> {
30
30
  const parameterConfigurations = configuration.parameterConfigurations ?? {} as Record<keyof T, ParameterConfiguration>;
@@ -34,9 +34,6 @@ export class Plan<T extends StringIndexedObject> {
34
34
  .map(([k, v]) => k)
35
35
  );
36
36
 
37
- const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
38
- const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
39
-
40
37
 
41
38
  // TODO: After adding in state files, need to calculate deletes here
42
39
  // Where current config exists and state config exists but desired config doesn't
@@ -51,9 +48,9 @@ export class Plan<T extends StringIndexedObject> {
51
48
  );
52
49
 
53
50
  let resourceOperation: ResourceOperation;
54
- if (!currentConfig && desiredConfig) {
51
+ if (!currentParameters && desiredParameters) {
55
52
  resourceOperation = ResourceOperation.CREATE;
56
- } else if (currentConfig && !desiredConfig) {
53
+ } else if (currentParameters && !desiredParameters) {
57
54
  resourceOperation = ResourceOperation.DESTROY;
58
55
  } else {
59
56
  resourceOperation = parameterChangeSet
@@ -50,8 +50,9 @@ describe('Resource parameters tests', () => {
50
50
  const resourceSpy = spy(resource);
51
51
  const result = await resourceSpy.apply(
52
52
  Plan.create<TestConfig>(
53
- { type: 'resource', propA: 'a', propB: 0, propC: 'b' },
53
+ { propA: 'a', propB: 0, propC: 'b' },
54
54
  null,
55
+ { type: 'resource' },
55
56
  { statefulMode: false },
56
57
  )
57
58
  );
@@ -136,6 +136,7 @@ describe('Resource tests', () => {
136
136
  const result = await resourceSpy.apply(
137
137
  Plan.create<TestConfig>(
138
138
  { type: 'resource', propA: 'a', propB: 0 },
139
+ null,
139
140
  { type: 'resource' },
140
141
  { statefulMode: false },
141
142
  )
@@ -154,8 +155,9 @@ describe('Resource tests', () => {
154
155
  const resourceSpy = spy(resource);
155
156
  const result = await resourceSpy.apply(
156
157
  Plan.create<TestConfig>(
158
+ null,
159
+ { propA: 'a', propB: 0 },
157
160
  { type: 'resource' },
158
- { type: 'resource', propA: 'a', propB: 0 },
159
161
  { statefulMode: true },
160
162
  )
161
163
  )
@@ -173,8 +175,9 @@ describe('Resource tests', () => {
173
175
  const resourceSpy = spy(resource);
174
176
  const result = await resourceSpy.apply(
175
177
  Plan.create<TestConfig>(
176
- { type: 'resource', propA: 'a', propB: 0 },
177
- { type: 'resource', propA: 'b', propB: -1 },
178
+ { propA: 'a', propB: 0 },
179
+ { propA: 'b', propB: -1 },
180
+ { type: 'resource' },
178
181
  { statefulMode: true },
179
182
  )
180
183
  );
@@ -66,7 +66,7 @@ export abstract class Resource<T extends StringIndexedObject> {
66
66
 
67
67
  // Short circuit here. If resource is non-existent, then there's no point checking stateful parameters
68
68
  if (currentParameters == null) {
69
- return Plan.create(desiredConfig, null, planConfiguration);
69
+ return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
70
70
  }
71
71
 
72
72
  this.validateRefreshResults(currentParameters, keysToRefresh);
@@ -76,7 +76,7 @@ export abstract class Resource<T extends StringIndexedObject> {
76
76
  for(const statefulParameter of statefulParameters) {
77
77
  const desiredValue = desiredParameters[statefulParameter.name];
78
78
 
79
- let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
79
+ let currentValue = await statefulParameter.refresh() ?? undefined;
80
80
 
81
81
  // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
82
82
  if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
@@ -87,8 +87,9 @@ export abstract class Resource<T extends StringIndexedObject> {
87
87
  }
88
88
 
89
89
  return Plan.create(
90
- desiredConfig,
91
- { ...currentParameters, ...resourceMetadata } as Partial<T> & ResourceConfig,
90
+ desiredParameters,
91
+ currentParameters as Partial<T>,
92
+ resourceMetadata,
92
93
  planConfiguration,
93
94
  )
94
95
  }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ArrayStatefulParameter, ArrayStatefulParameterConfiguration, } from './stateful-parameter.js';
3
+ import { Plan } from './plan.js';
4
+ import { spy } from 'sinon';
5
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
6
+
7
+ interface TestConfig {
8
+ propA: string[];
9
+ [x: string]: unknown;
10
+ }
11
+
12
+ class TestArrayParameter extends ArrayStatefulParameter<TestConfig, string> {
13
+ constructor(configuration?: ArrayStatefulParameterConfiguration<TestConfig>) {
14
+ super(configuration ?? {
15
+ name: 'propA'
16
+ })
17
+ }
18
+
19
+ async applyAddItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
20
+ async applyRemoveItem(item: string, plan: Plan<TestConfig>): Promise<void> {}
21
+
22
+ async refresh(): Promise<string[] | null> {
23
+ return null;
24
+ }
25
+ }
26
+
27
+
28
+ describe('Stateful parameter tests', () => {
29
+ it('applyAddItem is called the correct number of times', async () => {
30
+ const plan = Plan.create<TestConfig>(
31
+ { propA: ['a', 'b', 'c'] },
32
+ null,
33
+ { type: 'typeA' },
34
+ { statefulMode: false }
35
+ );
36
+
37
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
38
+ expect(plan.changeSet.parameterChanges.length).to.eq(1);
39
+
40
+ const testParameter = spy(new TestArrayParameter());
41
+ await testParameter.applyAdd(plan.desiredConfig.propA, plan);
42
+
43
+ expect(testParameter.applyAddItem.callCount).to.eq(3);
44
+ expect(testParameter.applyRemoveItem.called).to.be.false;
45
+ })
46
+
47
+ it('applyRemoveItem is called the correct number of times', async () => {
48
+ const plan = Plan.create<TestConfig>(
49
+ null,
50
+ { propA: ['a', 'b', 'c'] },
51
+ { type: 'typeA' },
52
+ { statefulMode: true }
53
+ );
54
+
55
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
56
+ expect(plan.changeSet.parameterChanges.length).to.eq(1);
57
+
58
+ const testParameter = spy(new TestArrayParameter());
59
+ await testParameter.applyRemove(plan.currentConfig.propA, plan);
60
+
61
+ expect(testParameter.applyAddItem.called).to.be.false;
62
+ expect(testParameter.applyRemoveItem.callCount).to.eq(3);
63
+ })
64
+
65
+ it('In stateless mode only applyAddItem is called only for modifies', async () => {
66
+ const plan = Plan.create<TestConfig>(
67
+ { propA: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
68
+ { propA: ['a', 'b', 'c'] },
69
+ { type: 'typeA' },
70
+ { statefulMode: true, parameterConfigurations: { propA: { isStatefulParameter: true }} }
71
+ );
72
+
73
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
74
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
75
+ name: 'propA',
76
+ previousValue: ['a', 'b', 'c'],
77
+ newValue: ['a', 'c', 'd', 'e', 'f'],
78
+ operation: ParameterOperation.MODIFY,
79
+ })
80
+
81
+ const testParameter = spy(new TestArrayParameter());
82
+ await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
83
+
84
+ expect(testParameter.applyAddItem.calledThrice).to.be.true;
85
+ expect(testParameter.applyRemoveItem.called).to.be.false;
86
+ })
87
+
88
+ it('isElementEqual is called for modifies', async () => {
89
+ const plan = Plan.create<TestConfig>(
90
+ { propA: ['9.12', '9.13'] }, // b to remove, d, e, f to add
91
+ { propA: ['9.12.9'] },
92
+ { type: 'typeA' },
93
+ { statefulMode: false, parameterConfigurations: { propA: { isStatefulParameter: true }} }
94
+ );
95
+
96
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
97
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
98
+ name: 'propA',
99
+ previousValue: ['9.12.9'],
100
+ newValue: ['9.12', '9.13'],
101
+ operation: ParameterOperation.MODIFY,
102
+ })
103
+
104
+ const testParameter = spy(new class extends TestArrayParameter {
105
+ constructor() {
106
+ super({
107
+ name: 'propA',
108
+ isElementEqual: (desired, current) => current.includes(desired),
109
+ });
110
+ }
111
+ });
112
+
113
+ await testParameter.applyModify(plan.desiredConfig.propA, plan.currentConfig.propA, false, plan);
114
+
115
+ expect(testParameter.applyAddItem.calledOnce).to.be.true;
116
+ expect(testParameter.applyRemoveItem.called).to.be.false;
117
+ })
118
+ })
@@ -21,7 +21,7 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
21
21
  this.configuration = configuration
22
22
  }
23
23
 
24
- abstract refresh(previousValue: V | null): Promise<V | null>;
24
+ abstract refresh(): Promise<V | null>;
25
25
 
26
26
  // TODO: Add an additional parameter here for what has actually changed.
27
27
  abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
@@ -30,9 +30,11 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
30
30
  }
31
31
 
32
32
  export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
33
+ configuration: ArrayStatefulParameterConfiguration<T>;
33
34
 
34
35
  constructor(configuration: ArrayStatefulParameterConfiguration<T>) {
35
36
  super(configuration);
37
+ this.configuration = configuration;
36
38
  }
37
39
 
38
40
  async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
@@ -42,8 +44,21 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
42
44
  }
43
45
 
44
46
  async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
45
- const valuesToAdd = newValues.filter((n) => !previousValues.includes(n));
46
- const valuesToRemove = previousValues.filter((n) => !newValues.includes(n));
47
+ const configuration = this.configuration as ArrayStatefulParameterConfiguration<T>;
48
+
49
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
50
+ if ((configuration).isElementEqual) {
51
+ return configuration.isElementEqual(n, p);
52
+ }
53
+ return n === p;
54
+ }));
55
+
56
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
57
+ if ((configuration).isElementEqual) {
58
+ return configuration.isElementEqual(n, p);
59
+ }
60
+ return n === p;
61
+ }));
47
62
 
48
63
  for (const value of valuesToAdd) {
49
64
  await this.applyAddItem(value, plan)
@@ -62,7 +77,7 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
62
77
  }
63
78
  }
64
79
 
65
- abstract refresh(previousValue: V[] | null): Promise<V[] | null>;
80
+ abstract refresh(): Promise<V[] | null>;
66
81
  abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
67
82
  abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
68
83
  }