codify-plugin-lib 1.0.47 → 1.0.48

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.
@@ -8,7 +8,7 @@ export declare class Plan<T extends StringIndexedObject> {
8
8
  constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
9
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
- static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
11
+ static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues: Partial<Record<keyof T, unknown>>): Plan<T>;
12
12
  get desiredConfig(): T;
13
13
  get currentConfig(): T;
14
14
  toResponse(): PlanResponseData;
@@ -45,18 +45,56 @@ export class Plan {
45
45
  getResourceType() {
46
46
  return this.resourceMetadata.type;
47
47
  }
48
- static fromResponse(data) {
48
+ static fromResponse(data, defaultValues) {
49
49
  if (!data) {
50
50
  throw new Error('Data is empty');
51
51
  }
52
- return new Plan(randomUUID(), new ChangeSet(data.operation, data.parameters.map(value => ({
53
- ...value,
54
- previousValue: null,
55
- }))), {
52
+ addDefaultValues();
53
+ return new Plan(randomUUID(), new ChangeSet(data.operation, data.parameters), {
56
54
  type: data.resourceType,
57
55
  name: data.resourceName,
58
- ...(data.parameters.reduce((prev, { name, newValue }) => Object.assign(prev, { [name]: newValue }), {}))
59
56
  });
57
+ function addDefaultValues() {
58
+ Object.entries(defaultValues)
59
+ .forEach(([key, defaultValue]) => {
60
+ const configValueExists = data
61
+ ?.parameters
62
+ .find((p) => p.name === key) !== undefined;
63
+ if (!configValueExists) {
64
+ switch (data?.operation) {
65
+ case ResourceOperation.CREATE: {
66
+ data?.parameters.push({
67
+ name: key,
68
+ operation: ParameterOperation.ADD,
69
+ previousValue: null,
70
+ newValue: defaultValue,
71
+ });
72
+ break;
73
+ }
74
+ case ResourceOperation.DESTROY: {
75
+ data?.parameters.push({
76
+ name: key,
77
+ operation: ParameterOperation.REMOVE,
78
+ previousValue: defaultValue,
79
+ newValue: null,
80
+ });
81
+ break;
82
+ }
83
+ case ResourceOperation.MODIFY:
84
+ case ResourceOperation.RECREATE:
85
+ case ResourceOperation.NOOP: {
86
+ data?.parameters.push({
87
+ name: key,
88
+ operation: ParameterOperation.NOOP,
89
+ previousValue: defaultValue,
90
+ newValue: defaultValue,
91
+ });
92
+ break;
93
+ }
94
+ }
95
+ }
96
+ });
97
+ }
60
98
  }
61
99
  get desiredConfig() {
62
100
  return {
@@ -65,7 +65,11 @@ export class Plugin {
65
65
  }
66
66
  return this.planStorage.get(planId);
67
67
  }
68
- return Plan.fromResponse(data.plan);
68
+ if (!planRequest?.resourceName || !this.resources.has(planRequest.resourceName)) {
69
+ throw new Error('Malformed plan. Resource name must be supplied');
70
+ }
71
+ const resource = this.resources.get(planRequest.resourceName);
72
+ return Plan.fromResponse(data.plan, resource?.defaultValues);
69
73
  }
70
74
  async crossValidateResources(configs) { }
71
75
  }
@@ -4,6 +4,7 @@ export type ErrorMessage = string;
4
4
  export interface ResourceParameterConfiguration {
5
5
  planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
6
6
  isEqual?: (desired: any, current: any) => boolean;
7
+ defaultValue?: unknown;
7
8
  }
8
9
  export interface ResourceConfiguration<T extends StringIndexedObject> {
9
10
  type: string;
@@ -9,6 +9,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
9
9
  readonly dependencies: string[];
10
10
  readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>;
11
11
  readonly configuration: ResourceConfiguration<T>;
12
+ readonly defaultValues: Partial<Record<keyof T, unknown>>;
12
13
  protected constructor(configuration: ResourceConfiguration<T>);
13
14
  onInitialize(): Promise<void>;
14
15
  plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
@@ -16,9 +17,11 @@ export declare abstract class Resource<T extends StringIndexedObject> {
16
17
  private _applyCreate;
17
18
  private _applyModify;
18
19
  private _applyDestroy;
19
- private generateParameterConfigurations;
20
+ private initializeParameterConfigurations;
21
+ private initializeDefaultValues;
20
22
  private validateResourceConfiguration;
21
23
  private validateRefreshResults;
24
+ private addDefaultValues;
22
25
  abstract validate(parameters: unknown): Promise<ValidationResult>;
23
26
  abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
24
27
  abstract applyCreate(plan: Plan<T>): Promise<void>;
@@ -7,11 +7,13 @@ export class Resource {
7
7
  dependencies;
8
8
  parameterConfigurations;
9
9
  configuration;
10
+ defaultValues;
10
11
  constructor(configuration) {
11
12
  this.validateResourceConfiguration(configuration);
12
13
  this.typeId = configuration.type;
13
14
  this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
14
- this.parameterConfigurations = this.generateParameterConfigurations(configuration);
15
+ this.parameterConfigurations = this.initializeParameterConfigurations(configuration);
16
+ this.defaultValues = this.initializeDefaultValues(configuration);
15
17
  this.dependencies = configuration.dependencies ?? [];
16
18
  this.configuration = configuration;
17
19
  }
@@ -22,6 +24,7 @@ export class Resource {
22
24
  parameterConfigurations: this.parameterConfigurations,
23
25
  };
24
26
  const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
27
+ this.addDefaultValues(desiredParameters);
25
28
  const resourceParameters = Object.fromEntries([
26
29
  ...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
27
30
  ]);
@@ -122,7 +125,7 @@ export class Resource {
122
125
  }
123
126
  await this.applyDestroy(plan);
124
127
  }
125
- generateParameterConfigurations(resourceConfiguration) {
128
+ initializeParameterConfigurations(resourceConfiguration) {
126
129
  const resourceParameters = Object.fromEntries(Object.entries(resourceConfiguration.parameterConfigurations ?? {})
127
130
  ?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }])));
128
131
  const statefulParameters = resourceConfiguration.statefulParameters
@@ -140,6 +143,14 @@ export class Resource {
140
143
  ...statefulParameters,
141
144
  };
142
145
  }
146
+ initializeDefaultValues(resourceConfiguration) {
147
+ if (!resourceConfiguration.parameterConfigurations) {
148
+ return {};
149
+ }
150
+ return Object.fromEntries(Object.entries(resourceConfiguration.parameterConfigurations)
151
+ .filter((p) => p[1]?.defaultValue !== undefined)
152
+ .map((config) => [config[0], config[1].defaultValue]));
153
+ }
143
154
  validateResourceConfiguration(data) {
144
155
  if (data.parameterConfigurations && data.statefulParameters) {
145
156
  const parameters = [...Object.keys(data.parameterConfigurations)];
@@ -163,6 +174,14 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
163
174
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
164
175
  }
165
176
  }
177
+ addDefaultValues(desired) {
178
+ Object.entries(this.defaultValues)
179
+ .forEach(([key, defaultValue]) => {
180
+ if (defaultValue !== undefined && desired[key] === undefined) {
181
+ desired[key] = defaultValue;
182
+ }
183
+ });
184
+ }
166
185
  async applyModify(parameterName, newValue, previousValue, allowDeletes, plan) { }
167
186
  ;
168
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Plan } from './plan.js';
3
+ import { TestResource } from './resource.test.js';
4
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
5
+ import { Resource } from './resource.js';
6
+
7
+ describe('Plan entity tests', () => {
8
+ it('Adds default values properly when plan is parsed from request (Create)', () => {
9
+ const resource = createResource();
10
+
11
+ const plan = Plan.fromResponse({
12
+ operation: ResourceOperation.CREATE,
13
+ resourceType: 'type',
14
+ parameters: [{
15
+ name: 'propB',
16
+ operation: ParameterOperation.ADD,
17
+ previousValue: null,
18
+ newValue: 'propBValue'
19
+ }]
20
+ }, resource.defaultValues);
21
+
22
+ expect(plan.currentConfig).toMatchObject({
23
+ type: 'type',
24
+ propA: null,
25
+ propB: null,
26
+ })
27
+
28
+ expect(plan.desiredConfig).toMatchObject({
29
+ type: 'type',
30
+ propA: 'defaultA',
31
+ propB: 'propBValue',
32
+ })
33
+
34
+ expect(plan.changeSet.parameterChanges
35
+ .every((pc) => pc.operation === ParameterOperation.ADD)
36
+ ).to.be.true;
37
+ })
38
+
39
+ it('Adds default values properly when plan is parsed from request (Destroy)', () => {
40
+ const resource = createResource();
41
+
42
+ const plan = Plan.fromResponse({
43
+ operation: ResourceOperation.DESTROY,
44
+ resourceType: 'type',
45
+ parameters: [{
46
+ name: 'propB',
47
+ operation: ParameterOperation.REMOVE,
48
+ previousValue: 'propBValue',
49
+ newValue: null,
50
+ }]
51
+ }, resource.defaultValues);
52
+
53
+ expect(plan.currentConfig).toMatchObject({
54
+ type: 'type',
55
+ propA: 'defaultA',
56
+ propB: 'propBValue',
57
+ })
58
+
59
+ expect(plan.desiredConfig).toMatchObject({
60
+ type: 'type',
61
+ propA: null,
62
+ propB: null,
63
+ })
64
+
65
+ expect(plan.changeSet.parameterChanges
66
+ .every((pc) => pc.operation === ParameterOperation.REMOVE)
67
+ ).to.be.true;
68
+ })
69
+
70
+ it('Adds default values properly when plan is parsed from request (No-op)', () => {
71
+ const resource = createResource();
72
+
73
+ const plan = Plan.fromResponse({
74
+ operation: ResourceOperation.NOOP,
75
+ resourceType: 'type',
76
+ parameters: [{
77
+ name: 'propB',
78
+ operation: ParameterOperation.NOOP,
79
+ previousValue: 'propBValue',
80
+ newValue: 'propBValue',
81
+ }]
82
+ }, resource.defaultValues);
83
+
84
+ expect(plan.currentConfig).toMatchObject({
85
+ type: 'type',
86
+ propA: 'defaultA',
87
+ propB: 'propBValue',
88
+ })
89
+
90
+ expect(plan.desiredConfig).toMatchObject({
91
+ type: 'type',
92
+ propA: 'defaultA',
93
+ propB: 'propBValue',
94
+ })
95
+
96
+ expect(plan.changeSet.parameterChanges
97
+ .every((pc) => pc.operation === ParameterOperation.NOOP)
98
+ ).to.be.true;
99
+ })
100
+
101
+ it('Does not add default value if a value has already been specified', () => {
102
+ const resource = createResource();
103
+
104
+ const plan = Plan.fromResponse({
105
+ operation: ResourceOperation.CREATE,
106
+ resourceType: 'type',
107
+ parameters: [{
108
+ name: 'propB',
109
+ operation: ParameterOperation.ADD,
110
+ previousValue: null,
111
+ newValue: 'propBValue',
112
+ }, {
113
+ name: 'propA',
114
+ operation: ParameterOperation.ADD,
115
+ previousValue: null,
116
+ newValue: 'propAValue',
117
+ }]
118
+ }, resource.defaultValues);
119
+
120
+ expect(plan.currentConfig).toMatchObject({
121
+ type: 'type',
122
+ propA: null,
123
+ propB: null,
124
+ })
125
+
126
+ expect(plan.desiredConfig).toMatchObject({
127
+ type: 'type',
128
+ propA: 'propAValue',
129
+ propB: 'propBValue',
130
+ })
131
+
132
+ expect(plan.changeSet.parameterChanges
133
+ .every((pc) => pc.operation === ParameterOperation.ADD)
134
+ ).to.be.true;
135
+ })
136
+ })
137
+
138
+ function createResource(): Resource<any> {
139
+ return new class extends TestResource {
140
+ constructor() {
141
+ super({
142
+ type: 'type',
143
+ parameterConfigurations: {
144
+ propA: {
145
+ defaultValue: 'defaultA'
146
+ }
147
+ }
148
+ });
149
+ }
150
+ }
151
+ }
@@ -75,29 +75,70 @@ export class Plan<T extends StringIndexedObject> {
75
75
  return this.resourceMetadata.type
76
76
  }
77
77
 
78
- static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T> {
78
+ static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues: Partial<Record<keyof T, unknown>>): Plan<T> {
79
79
  if (!data) {
80
80
  throw new Error('Data is empty');
81
81
  }
82
82
 
83
+ addDefaultValues();
84
+
83
85
  return new Plan(
84
86
  randomUUID(),
85
87
  new ChangeSet<T>(
86
88
  data.operation,
87
- data.parameters.map(value => ({
88
- ...value,
89
- previousValue: null,
90
- })),
89
+ data.parameters
91
90
  ),
92
91
  {
93
92
  type: data.resourceType,
94
93
  name: data.resourceName,
95
- ...(data.parameters.reduce(
96
- (prev, { name, newValue }) => Object.assign(prev, { [name]: newValue }),
97
- {}
98
- ))
99
94
  },
100
95
  );
96
+
97
+ function addDefaultValues(): void {
98
+ Object.entries(defaultValues)
99
+ .forEach(([key, defaultValue]) => {
100
+ const configValueExists = data
101
+ ?.parameters
102
+ .find((p) => p.name === key) !== undefined;
103
+
104
+ if (!configValueExists) {
105
+ switch (data?.operation) {
106
+ case ResourceOperation.CREATE: {
107
+ data?.parameters.push({
108
+ name: key,
109
+ operation: ParameterOperation.ADD,
110
+ previousValue: null,
111
+ newValue: defaultValue,
112
+ });
113
+ break;
114
+ }
115
+
116
+ case ResourceOperation.DESTROY: {
117
+ data?.parameters.push({
118
+ name: key,
119
+ operation: ParameterOperation.REMOVE,
120
+ previousValue: defaultValue,
121
+ newValue: null,
122
+ });
123
+ break;
124
+ }
125
+
126
+ case ResourceOperation.MODIFY:
127
+ case ResourceOperation.RECREATE:
128
+ case ResourceOperation.NOOP: {
129
+ data?.parameters.push({
130
+ name: key,
131
+ operation: ParameterOperation.NOOP,
132
+ previousValue: defaultValue,
133
+ newValue: defaultValue,
134
+ });
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ });
140
+ }
141
+
101
142
  }
102
143
 
103
144
  get desiredConfig(): T {
@@ -94,7 +94,12 @@ export class Plugin {
94
94
  return this.planStorage.get(planId)!
95
95
  }
96
96
 
97
- return Plan.fromResponse(data.plan);
97
+ if (!planRequest?.resourceName || !this.resources.has(planRequest.resourceName)) {
98
+ throw new Error('Malformed plan. Resource name must be supplied');
99
+ }
100
+
101
+ const resource = this.resources.get(planRequest.resourceName);
102
+ return Plan.fromResponse(data.plan, resource?.defaultValues!);
98
103
  }
99
104
 
100
105
  protected async crossValidateResources(configs: ResourceConfig[]): Promise<void> {}
@@ -17,6 +17,10 @@ export interface ResourceParameterConfiguration {
17
17
  * @param current
18
18
  */
19
19
  isEqual?: (desired: any, current: any) => boolean;
20
+ /**
21
+ * Default value for the parameter. If a value is not provided in the config, the library will use this value.
22
+ */
23
+ defaultValue?: unknown,
20
24
  }
21
25
 
22
26
  /**
@@ -293,4 +293,61 @@ describe('Resource tests', () => {
293
293
  }).to.not.throw;
294
294
  })
295
295
 
296
+ it('Allows default values to be added', async () => {
297
+ const resource = new class extends TestResource {
298
+ constructor() {
299
+ super({
300
+ type: 'type',
301
+ parameterConfigurations: {
302
+ propA: { defaultValue: 'propADefault' }
303
+ }
304
+ });
305
+ }
306
+
307
+ // @ts-ignore
308
+ async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
309
+ expect(desired.has('propA')).to.be.true;
310
+ expect(desired.get('propA')).to.be.eq('propADefault');
311
+
312
+ return {
313
+ propA: 'propAAfter'
314
+ };
315
+ }
316
+ }
317
+
318
+ const plan = await resource.plan({ type: 'resource'})
319
+ expect(plan.currentConfig.propA).to.eq('propAAfter');
320
+ expect(plan.desiredConfig.propA).to.eq('propADefault');
321
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
322
+
323
+ })
324
+
325
+ it('Allows default values to be added (ignore default value if already present)', async () => {
326
+ const resource = new class extends TestResource {
327
+ constructor() {
328
+ super({
329
+ type: 'type',
330
+ parameterConfigurations: {
331
+ propA: { defaultValue: 'propADefault' }
332
+ }
333
+ });
334
+ }
335
+
336
+ // @ts-ignore
337
+ async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
338
+ expect(desired.has('propA')).to.be.true;
339
+ expect(desired.get('propA')).to.be.eq('propA');
340
+
341
+ return {
342
+ propA: 'propAAfter'
343
+ };
344
+ }
345
+ }
346
+
347
+ const plan = await resource.plan({ type: 'resource', propA: 'propA'})
348
+ expect(plan.currentConfig.propA).to.eq('propAAfter');
349
+ expect(plan.desiredConfig.propA).to.eq('propA');
350
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
351
+
352
+ })
296
353
  });
@@ -20,13 +20,15 @@ export abstract class Resource<T extends StringIndexedObject> {
20
20
  readonly dependencies: string[]; // TODO: Change this to a string
21
21
  readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>
22
22
  readonly configuration: ResourceConfiguration<T>;
23
+ readonly defaultValues: Partial<Record<keyof T, unknown>>;
23
24
 
24
25
  protected constructor(configuration: ResourceConfiguration<T>) {
25
26
  this.validateResourceConfiguration(configuration);
26
27
 
27
28
  this.typeId = configuration.type;
28
29
  this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
29
- this.parameterConfigurations = this.generateParameterConfigurations(configuration);
30
+ this.parameterConfigurations = this.initializeParameterConfigurations(configuration);
31
+ this.defaultValues = this.initializeDefaultValues(configuration);
30
32
 
31
33
  this.dependencies = configuration.dependencies ?? [];
32
34
  this.configuration = configuration;
@@ -47,6 +49,8 @@ export abstract class Resource<T extends StringIndexedObject> {
47
49
 
48
50
  const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
49
51
 
52
+ this.addDefaultValues(desiredParameters);
53
+
50
54
  const resourceParameters = Object.fromEntries([
51
55
  ...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
52
56
  ]) as Partial<T>;
@@ -183,7 +187,7 @@ export abstract class Resource<T extends StringIndexedObject> {
183
187
  await this.applyDestroy(plan);
184
188
  }
185
189
 
186
- private generateParameterConfigurations(
190
+ private initializeParameterConfigurations(
187
191
  resourceConfiguration: ResourceConfiguration<T>
188
192
  ): Record<keyof T, ParameterConfiguration> {
189
193
  const resourceParameters = Object.fromEntries(
@@ -209,6 +213,20 @@ export abstract class Resource<T extends StringIndexedObject> {
209
213
 
210
214
  }
211
215
 
216
+ private initializeDefaultValues(
217
+ resourceConfiguration: ResourceConfiguration<T>
218
+ ): Partial<Record<keyof T, unknown>> {
219
+ if (!resourceConfiguration.parameterConfigurations) {
220
+ return {};
221
+ }
222
+
223
+ return Object.fromEntries(
224
+ Object.entries(resourceConfiguration.parameterConfigurations)
225
+ .filter((p) => p[1]?.defaultValue !== undefined)
226
+ .map((config) => [config[0], config[1]!.defaultValue])
227
+ ) as Partial<Record<keyof T, unknown>>;
228
+ }
229
+
212
230
  private validateResourceConfiguration(data: ResourceConfiguration<T>) {
213
231
  // Stateful parameters are configured within the object not in the resource.
214
232
  if (data.parameterConfigurations && data.statefulParameters) {
@@ -220,8 +238,6 @@ export abstract class Resource<T extends StringIndexedObject> {
220
238
  throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
221
239
  }
222
240
  }
223
-
224
-
225
241
  }
226
242
 
227
243
  private validateRefreshResults(refresh: Partial<T> | null, desiredMap: Map<keyof T, T[keyof T]>) {
@@ -242,6 +258,16 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
242
258
  }
243
259
  }
244
260
 
261
+ private addDefaultValues(desired: Partial<T>): void {
262
+ Object.entries(this.defaultValues)
263
+ .forEach(([key, defaultValue]) => {
264
+ if (defaultValue !== undefined && desired[key as any] === undefined) {
265
+ // @ts-ignore
266
+ desired[key] = defaultValue;
267
+ }
268
+ });
269
+ }
270
+
245
271
  abstract validate(parameters: unknown): Promise<ValidationResult>;
246
272
 
247
273
  abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;