codify-plugin-lib 1.0.36 → 1.0.38

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.
@@ -1,16 +1,24 @@
1
- import { ParameterOperation, ResourceConfig, ResourceOperation } from 'codify-schemas';
2
- export interface ParameterChange {
3
- name: string;
1
+ import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
2
+ import { ParameterConfiguration } from './plan-types.js';
3
+ export interface ParameterChange<T extends StringIndexedObject> {
4
+ name: keyof T & string;
4
5
  operation: ParameterOperation;
5
6
  previousValue: any | null;
6
7
  newValue: any | null;
7
8
  }
8
- export declare class ChangeSet {
9
+ export declare class ChangeSet<T extends StringIndexedObject> {
9
10
  operation: ResourceOperation;
10
- parameterChanges: Array<ParameterChange>;
11
- constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange>);
12
- static createForNullCurrentConfig(desiredConfig: ResourceConfig): ChangeSet;
13
- static calculateParameterChangeSet(prev: ResourceConfig, next: ResourceConfig): ParameterChange[];
11
+ parameterChanges: Array<ParameterChange<T>>;
12
+ constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
13
+ get desiredParameters(): T;
14
+ get currentParameters(): T;
15
+ static newCreate<T extends {}>(desiredConfig: T): ChangeSet<StringIndexedObject>;
16
+ static calculateParameterChangeSet<T extends StringIndexedObject>(desired: T | null, current: T | null, options: {
17
+ statefulMode: boolean;
18
+ parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
19
+ }): ParameterChange<T>[];
14
20
  static combineResourceOperations(prev: ResourceOperation, next: ResourceOperation): ResourceOperation;
15
- static isSame(a: unknown, b: unknown): boolean;
21
+ static isSame(a: unknown, b: unknown, isEqual?: (a: unknown, b: unknown) => boolean): boolean;
22
+ private static calculateStatefulModeChangeSet;
23
+ private static calculateStatelessModeChangeSet;
16
24
  }
@@ -6,7 +6,21 @@ export class ChangeSet {
6
6
  this.operation = operation;
7
7
  this.parameterChanges = parameterChanges;
8
8
  }
9
- static createForNullCurrentConfig(desiredConfig) {
9
+ get desiredParameters() {
10
+ return this.parameterChanges
11
+ .reduce((obj, pc) => ({
12
+ ...obj,
13
+ [pc.name]: pc.newValue,
14
+ }), {});
15
+ }
16
+ get currentParameters() {
17
+ return this.parameterChanges
18
+ .reduce((obj, pc) => ({
19
+ ...obj,
20
+ [pc.name]: pc.previousValue,
21
+ }), {});
22
+ }
23
+ static newCreate(desiredConfig) {
10
24
  const parameterChangeSet = Object.entries(desiredConfig)
11
25
  .filter(([k,]) => k !== 'type' && k !== 'name')
12
26
  .map(([k, v]) => {
@@ -19,47 +33,76 @@ export class ChangeSet {
19
33
  });
20
34
  return new ChangeSet(ResourceOperation.CREATE, parameterChangeSet);
21
35
  }
22
- static calculateParameterChangeSet(prev, next) {
36
+ static calculateParameterChangeSet(desired, current, options) {
37
+ if (options.statefulMode) {
38
+ return ChangeSet.calculateStatefulModeChangeSet(desired, current, options.parameterConfigurations);
39
+ }
40
+ else {
41
+ return ChangeSet.calculateStatelessModeChangeSet(desired, current, options.parameterConfigurations);
42
+ }
43
+ }
44
+ static combineResourceOperations(prev, next) {
45
+ const orderOfOperations = [
46
+ ResourceOperation.NOOP,
47
+ ResourceOperation.MODIFY,
48
+ ResourceOperation.RECREATE,
49
+ ResourceOperation.CREATE,
50
+ ResourceOperation.DESTROY,
51
+ ];
52
+ const indexPrev = orderOfOperations.indexOf(prev);
53
+ const indexNext = orderOfOperations.indexOf(next);
54
+ return orderOfOperations[Math.max(indexPrev, indexNext)];
55
+ }
56
+ static isSame(a, b, isEqual) {
57
+ if (isEqual) {
58
+ return isEqual(a, b);
59
+ }
60
+ if (Array.isArray(a) && Array.isArray(b)) {
61
+ const sortedPrev = a.map((x) => x).sort();
62
+ const sortedNext = b.map((x) => x).sort();
63
+ return JSON.stringify(sortedPrev) === JSON.stringify(sortedNext);
64
+ }
65
+ return a === b;
66
+ }
67
+ static calculateStatefulModeChangeSet(desired, current, parameterConfigurations) {
23
68
  const parameterChangeSet = new Array();
24
- const filteredPrev = Object.fromEntries(Object.entries(prev)
25
- .filter(([k,]) => k !== 'type' && k !== 'name'));
26
- const filteredNext = Object.fromEntries(Object.entries(next)
27
- .filter(([k,]) => k !== 'type' && k !== 'name'));
28
- for (const [k, v] of Object.entries(filteredPrev)) {
29
- if (!filteredNext[k]) {
69
+ const _desired = { ...desired };
70
+ const _current = { ...current };
71
+ for (const [k, v] of Object.entries(_current)) {
72
+ if (_desired[k] == null) {
30
73
  parameterChangeSet.push({
31
74
  name: k,
32
75
  previousValue: v,
33
76
  newValue: null,
34
77
  operation: ParameterOperation.REMOVE,
35
78
  });
36
- delete filteredPrev[k];
79
+ delete _current[k];
37
80
  continue;
38
81
  }
39
- if (!ChangeSet.isSame(filteredPrev[k], filteredNext[k])) {
82
+ if (!ChangeSet.isSame(_current[k], _desired[k], parameterConfigurations?.[k]?.isEqual)) {
40
83
  parameterChangeSet.push({
41
84
  name: k,
42
85
  previousValue: v,
43
- newValue: filteredNext[k],
86
+ newValue: _desired[k],
44
87
  operation: ParameterOperation.MODIFY,
45
88
  });
46
- delete filteredPrev[k];
47
- delete filteredNext[k];
89
+ delete _current[k];
90
+ delete _desired[k];
48
91
  continue;
49
92
  }
50
93
  parameterChangeSet.push({
51
94
  name: k,
52
95
  previousValue: v,
53
- newValue: filteredNext[k],
96
+ newValue: _desired[k],
54
97
  operation: ParameterOperation.NOOP,
55
98
  });
56
- delete filteredPrev[k];
57
- delete filteredNext[k];
99
+ delete _current[k];
100
+ delete _desired[k];
58
101
  }
59
- if (Object.keys(filteredPrev).length !== 0) {
102
+ if (Object.keys(_current).length !== 0) {
60
103
  throw Error('Diff algorithm error');
61
104
  }
62
- for (const [k, v] of Object.entries(filteredNext)) {
105
+ for (const [k, v] of Object.entries(_desired)) {
63
106
  parameterChangeSet.push({
64
107
  name: k,
65
108
  previousValue: null,
@@ -69,24 +112,36 @@ export class ChangeSet {
69
112
  }
70
113
  return parameterChangeSet;
71
114
  }
72
- static combineResourceOperations(prev, next) {
73
- const orderOfOperations = [
74
- ResourceOperation.NOOP,
75
- ResourceOperation.MODIFY,
76
- ResourceOperation.RECREATE,
77
- ResourceOperation.CREATE,
78
- ResourceOperation.DESTROY,
79
- ];
80
- const indexPrev = orderOfOperations.indexOf(prev);
81
- const indexNext = orderOfOperations.indexOf(next);
82
- return orderOfOperations[Math.max(indexPrev, indexNext)];
83
- }
84
- static isSame(a, b) {
85
- if (Array.isArray(a) && Array.isArray(b)) {
86
- const sortedPrev = a.map((x) => x).sort();
87
- const sortedNext = b.map((x) => x).sort();
88
- return JSON.stringify(sortedPrev) === JSON.stringify(sortedNext);
115
+ static calculateStatelessModeChangeSet(desired, current, parameterConfigurations) {
116
+ const parameterChangeSet = new Array();
117
+ const _desired = { ...desired };
118
+ const _current = { ...current };
119
+ for (const [k, v] of Object.entries(_desired)) {
120
+ if (_current[k] == null) {
121
+ parameterChangeSet.push({
122
+ name: k,
123
+ previousValue: null,
124
+ newValue: v,
125
+ operation: ParameterOperation.ADD,
126
+ });
127
+ continue;
128
+ }
129
+ if (!ChangeSet.isSame(_current[k], _desired[k], parameterConfigurations?.[k]?.isEqual)) {
130
+ parameterChangeSet.push({
131
+ name: k,
132
+ previousValue: _current[k],
133
+ newValue: _desired[k],
134
+ operation: ParameterOperation.MODIFY,
135
+ });
136
+ continue;
137
+ }
138
+ parameterChangeSet.push({
139
+ name: k,
140
+ previousValue: v,
141
+ newValue: v,
142
+ operation: ParameterOperation.NOOP,
143
+ });
89
144
  }
90
- return a === b;
145
+ return parameterChangeSet;
91
146
  }
92
147
  }
@@ -0,0 +1,11 @@
1
+ import { ResourceOperation } from 'codify-schemas';
2
+ export interface ParameterConfiguration {
3
+ planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
4
+ isEqual?: (a: any, b: any) => boolean;
5
+ isArrayElementEqual?: (a: any, b: any) => boolean;
6
+ isStatefulParameter?: boolean;
7
+ }
8
+ export interface PlanConfiguration {
9
+ statefulMode: boolean;
10
+ parameterConfigurations?: Record<string, ParameterConfiguration>;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,11 +1,15 @@
1
1
  import { ChangeSet } from './change-set.js';
2
- import { PlanResponseData, ResourceConfig } from 'codify-schemas';
3
- export declare class Plan<T extends ResourceConfig> {
2
+ import { ApplyRequestData, PlanResponseData, ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
+ import { PlanConfiguration } from './plan-types.js';
4
+ export declare class Plan<T extends StringIndexedObject> {
4
5
  id: string;
5
- changeSet: ChangeSet;
6
- resourceConfig: T;
7
- constructor(id: string, changeSet: ChangeSet, resourceConfig: T);
8
- static create<T extends ResourceConfig>(changeSet: ChangeSet, resourceConfig: T): Plan<T>;
6
+ changeSet: ChangeSet<T>;
7
+ resourceMetadata: ResourceConfig;
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): Plan<T>;
9
10
  getResourceType(): string;
11
+ static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
12
+ get desiredConfig(): T;
13
+ get currentConfig(): T;
10
14
  toResponse(): PlanResponseData;
11
15
  }
@@ -1,25 +1,84 @@
1
+ import { ChangeSet } from './change-set.js';
2
+ import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
1
3
  import { randomUUID } from 'crypto';
4
+ import { splitUserConfig } from '../utils/utils.js';
2
5
  export class Plan {
3
6
  id;
4
7
  changeSet;
5
- resourceConfig;
6
- constructor(id, changeSet, resourceConfig) {
8
+ resourceMetadata;
9
+ constructor(id, changeSet, resourceMetadata) {
7
10
  this.id = id;
8
11
  this.changeSet = changeSet;
9
- this.resourceConfig = resourceConfig;
12
+ this.resourceMetadata = resourceMetadata;
10
13
  }
11
- static create(changeSet, resourceConfig) {
12
- return new Plan(randomUUID(), changeSet, resourceConfig);
14
+ static create(desiredConfig, currentConfig, configuration) {
15
+ const parameterConfigurations = configuration.parameterConfigurations ?? {};
16
+ const statefulParameterNames = new Set([...Object.entries(parameterConfigurations)]
17
+ .filter(([k, v]) => v.isStatefulParameter)
18
+ .map(([k, v]) => k));
19
+ const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
20
+ const { parameters: currentParameters } = currentConfig ? splitUserConfig(currentConfig) : { parameters: {} };
21
+ const parameterChangeSet = ChangeSet.calculateParameterChangeSet(desiredParameters, currentParameters, { statefulMode: configuration.statefulMode, parameterConfigurations });
22
+ let resourceOperation;
23
+ if (!currentConfig && desiredConfig) {
24
+ resourceOperation = ResourceOperation.CREATE;
25
+ }
26
+ else if (currentConfig && !desiredConfig) {
27
+ resourceOperation = ResourceOperation.DESTROY;
28
+ }
29
+ else {
30
+ resourceOperation = parameterChangeSet
31
+ .filter((change) => change.operation !== ParameterOperation.NOOP)
32
+ .reduce((operation, curr) => {
33
+ let newOperation;
34
+ if (statefulParameterNames.has(curr.name)) {
35
+ newOperation = ResourceOperation.MODIFY;
36
+ }
37
+ else if (parameterConfigurations[curr.name]?.planOperation) {
38
+ newOperation = parameterConfigurations[curr.name].planOperation;
39
+ }
40
+ else {
41
+ newOperation = ResourceOperation.RECREATE;
42
+ }
43
+ return ChangeSet.combineResourceOperations(operation, newOperation);
44
+ }, ResourceOperation.NOOP);
45
+ }
46
+ return new Plan(randomUUID(), new ChangeSet(resourceOperation, parameterChangeSet), resourceMetadata);
13
47
  }
14
48
  getResourceType() {
15
- return this.resourceConfig.type;
49
+ return this.resourceMetadata.type;
50
+ }
51
+ static fromResponse(data) {
52
+ if (!data) {
53
+ throw new Error('Data is empty');
54
+ }
55
+ return new Plan(randomUUID(), new ChangeSet(data.operation, data.parameters.map(value => ({
56
+ ...value,
57
+ previousValue: null,
58
+ }))), {
59
+ type: data.resourceType,
60
+ name: data.resourceName,
61
+ ...(data.parameters.reduce((prev, { name, newValue }) => Object.assign(prev, { [name]: newValue }), {}))
62
+ });
63
+ }
64
+ get desiredConfig() {
65
+ return {
66
+ ...this.resourceMetadata,
67
+ ...this.changeSet.desiredParameters,
68
+ };
69
+ }
70
+ get currentConfig() {
71
+ return {
72
+ ...this.resourceMetadata,
73
+ ...this.changeSet.currentParameters,
74
+ };
16
75
  }
17
76
  toResponse() {
18
77
  return {
19
78
  planId: this.id,
20
79
  operation: this.changeSet.operation,
21
- resourceName: this.resourceConfig.name,
22
- resourceType: this.resourceConfig.type,
80
+ resourceName: this.resourceMetadata.name,
81
+ resourceType: this.resourceMetadata.type,
23
82
  parameters: this.changeSet.parameterChanges,
24
83
  };
25
84
  }
@@ -9,5 +9,6 @@ export declare class Plugin {
9
9
  validate(data: ValidateRequestData): Promise<ValidateResponseData>;
10
10
  plan(data: PlanRequestData): Promise<PlanResponseData>;
11
11
  apply(data: ApplyRequestData): Promise<void>;
12
+ private resolvePlan;
12
13
  protected crossValidateResources(configs: ResourceConfig[]): Promise<void>;
13
14
  }
@@ -1,3 +1,4 @@
1
+ import { Plan } from './plan.js';
1
2
  export class Plugin {
2
3
  resources;
3
4
  planStorage;
@@ -12,26 +13,27 @@ export class Plugin {
12
13
  return {
13
14
  resourceDefinitions: [...this.resources.values()]
14
15
  .map((r) => ({
15
- type: r.getTypeId(),
16
+ type: r.typeId,
16
17
  dependencies: r.getDependencyTypeIds(),
17
18
  }))
18
19
  };
19
20
  }
20
21
  async validate(data) {
21
- const totalErrors = [];
22
+ const validationResults = [];
22
23
  for (const config of data.configs) {
23
24
  if (!this.resources.has(config.type)) {
24
25
  throw new Error(`Resource type not found: ${config.type}`);
25
26
  }
26
- const error = await this.resources.get(config.type).validate(config);
27
- if (error) {
28
- totalErrors.push(...error);
29
- }
27
+ const validateResult = await this.resources.get(config.type).validate(config);
28
+ validationResults.push({
29
+ ...validateResult,
30
+ resourceType: config.type,
31
+ resourceName: config.name,
32
+ });
30
33
  }
31
34
  await this.crossValidateResources(data.configs);
32
35
  return {
33
- isValid: true,
34
- errors: totalErrors,
36
+ validationResults
35
37
  };
36
38
  }
37
39
  async plan(data) {
@@ -43,16 +45,25 @@ export class Plugin {
43
45
  return plan.toResponse();
44
46
  }
45
47
  async apply(data) {
46
- const { planId } = data;
47
- const plan = this.planStorage.get(planId);
48
- if (!plan) {
49
- throw new Error(`Plan with id: ${planId} was not found`);
48
+ if (!data.planId && !data.plan) {
49
+ throw new Error(`For applies either plan or planId must be supplied`);
50
50
  }
51
+ const plan = this.resolvePlan(data);
51
52
  const resource = this.resources.get(plan.getResourceType());
52
53
  if (!resource) {
53
54
  throw new Error('Malformed plan with resource that cannot be found');
54
55
  }
55
56
  await resource.apply(plan);
56
57
  }
58
+ resolvePlan(data) {
59
+ const { planId, plan: planRequest } = data;
60
+ if (planId) {
61
+ if (!this.planStorage.has(planId)) {
62
+ throw new Error(`Plan with id: ${planId} was not found`);
63
+ }
64
+ return this.planStorage.get(planId);
65
+ }
66
+ return Plan.fromResponse(data.plan);
67
+ }
57
68
  async crossValidateResources(configs) { }
58
69
  }
@@ -0,0 +1,24 @@
1
+ import { StatefulParameter } from './stateful-parameter.js';
2
+ import { ResourceOperation, StringIndexedObject } from 'codify-schemas';
3
+ import { Resource } from './resource.js';
4
+ export type ErrorMessage = string;
5
+ export interface ResourceParameterConfiguration {
6
+ planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
7
+ isEqual?: (a: any, b: any) => boolean;
8
+ }
9
+ export interface ResourceConfiguration<T extends StringIndexedObject> {
10
+ type: string;
11
+ callStatefulParameterRemoveOnDestroy?: boolean;
12
+ dependencies?: Resource<any>[];
13
+ statefulParameters?: Array<StatefulParameter<T, T[keyof T]>>;
14
+ parameterConfigurations?: Partial<Record<keyof T, ResourceParameterConfiguration>>;
15
+ }
16
+ export interface ResourceDefinition {
17
+ [x: string]: {
18
+ type: string;
19
+ };
20
+ }
21
+ export interface ValidationResult {
22
+ isValid: boolean;
23
+ errors?: unknown[];
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,23 +1,28 @@
1
- import { ResourceConfig, ResourceOperation } from 'codify-schemas';
2
- import { ParameterChange } from './change-set.js';
1
+ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
2
  import { Plan } from './plan.js';
4
3
  import { StatefulParameter } from './stateful-parameter.js';
5
- export type ErrorMessage = string;
6
- export declare abstract class Resource<T extends ResourceConfig> {
7
- private dependencies;
8
- private statefulParameters;
9
- constructor(dependencies?: Resource<any>[]);
10
- abstract getTypeId(): string;
4
+ import { ResourceConfiguration, ValidationResult } from './resource-types.js';
5
+ import { ParameterConfiguration } from './plan-types.js';
6
+ export declare abstract class Resource<T extends StringIndexedObject> {
7
+ readonly typeId: string;
8
+ readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
9
+ readonly dependencies: Resource<any>[];
10
+ readonly parameterConfigurations: Record<string, ParameterConfiguration>;
11
+ private readonly options;
12
+ protected constructor(configuration: ResourceConfiguration<T>);
11
13
  getDependencyTypeIds(): string[];
12
14
  onInitialize(): Promise<void>;
13
- plan(desiredConfig: T): Promise<Plan<T>>;
15
+ plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
14
16
  apply(plan: Plan<T>): Promise<void>;
15
- protected registerStatefulParameter(parameter: StatefulParameter<T, keyof T>): void;
16
- abstract validate(config: unknown): Promise<ErrorMessage[] | undefined>;
17
- abstract getCurrentConfig(desiredConfig: T): Promise<T | null>;
18
- abstract calculateOperation(change: ParameterChange): ResourceOperation.MODIFY | ResourceOperation.RECREATE;
17
+ private _applyCreate;
18
+ private _applyModify;
19
+ private _applyDestroy;
20
+ private generateParameterConfigurations;
21
+ private validateResourceConfiguration;
22
+ private validateRefreshResults;
23
+ abstract validate(config: unknown): Promise<ValidationResult>;
24
+ abstract refresh(keys: Set<keyof T>): Promise<Partial<T> | null>;
19
25
  abstract applyCreate(plan: Plan<T>): Promise<void>;
20
- abstract applyModify(plan: Plan<T>): Promise<void>;
21
- abstract applyRecreate(plan: Plan<T>): Promise<void>;
26
+ applyModify(parameterName: keyof T, newValue: unknown, previousValue: unknown, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
22
27
  abstract applyDestroy(plan: Plan<T>): Promise<void>;
23
28
  }