codify-plugin-lib 1.0.46 → 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,11 +17,13 @@ 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
- abstract refresh(keys: Set<keyof T>): Promise<Partial<T> | null>;
26
+ abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
24
27
  abstract applyCreate(plan: Plan<T>): Promise<void>;
25
28
  applyModify(parameterName: keyof T, newValue: unknown, previousValue: unknown, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
26
29
  abstract applyDestroy(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,20 +24,21 @@ 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
  ]);
28
31
  const statefulParameters = [...this.statefulParameters.values()]
29
32
  .filter((sp) => desiredParameters[sp.name] !== undefined);
30
- const keysToRefresh = new Set(Object.keys(resourceParameters));
31
- const currentParameters = await this.refresh(keysToRefresh);
33
+ const entriesToRefresh = new Map(Object.entries(resourceParameters));
34
+ const currentParameters = await this.refresh(entriesToRefresh);
32
35
  if (currentParameters == null) {
33
36
  return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
34
37
  }
35
- this.validateRefreshResults(currentParameters, keysToRefresh);
38
+ this.validateRefreshResults(currentParameters, entriesToRefresh);
36
39
  for (const statefulParameter of statefulParameters) {
37
40
  const desiredValue = desiredParameters[statefulParameter.name];
38
- let currentValue = await statefulParameter.refresh() ?? undefined;
41
+ let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
39
42
  if (Array.isArray(currentValue)
40
43
  && Array.isArray(desiredValue)
41
44
  && !planConfiguration.statefulMode
@@ -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)];
@@ -150,10 +161,11 @@ export class Resource {
150
161
  }
151
162
  }
152
163
  }
153
- validateRefreshResults(refresh, desiredKeys) {
164
+ validateRefreshResults(refresh, desiredMap) {
154
165
  if (!refresh) {
155
166
  return;
156
167
  }
168
+ const desiredKeys = new Set(desiredMap.keys());
157
169
  const refreshKeys = new Set(Object.keys(refresh));
158
170
  if (!setsEqual(desiredKeys, refreshKeys)) {
159
171
  throw new Error(`Resource ${this.configuration.type}
@@ -162,6 +174,14 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
162
174
  Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
163
175
  }
164
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
+ }
165
185
  async applyModify(parameterName, newValue, previousValue, allowDeletes, plan) { }
166
186
  ;
167
187
  }
@@ -13,7 +13,7 @@ export declare abstract class StatefulParameter<T extends StringIndexedObject, V
13
13
  readonly name: keyof T;
14
14
  readonly configuration: StatefulParameterConfiguration<T>;
15
15
  protected constructor(configuration: StatefulParameterConfiguration<T>);
16
- abstract refresh(): Promise<V | null>;
16
+ abstract refresh(desired: V | null): Promise<V | null>;
17
17
  abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
18
18
  abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
19
19
  abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
@@ -24,7 +24,7 @@ export declare abstract class ArrayStatefulParameter<T extends StringIndexedObje
24
24
  applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
25
25
  applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
26
26
  applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
27
- abstract refresh(): Promise<V[] | null>;
27
+ abstract refresh(desired: V[] | null): Promise<V[] | null>;
28
28
  abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
29
29
  abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
30
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "ajv": "^8.12.0",
16
16
  "ajv-formats": "^2.1.1",
17
- "codify-schemas": "1.0.32",
17
+ "codify-schemas": "1.0.33",
18
18
  "@npmcli/promise-spawn": "^7.0.1"
19
19
  },
20
20
  "devDependencies": {
@@ -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>;
@@ -56,22 +60,22 @@ export abstract class Resource<T extends StringIndexedObject> {
56
60
 
57
61
  // Refresh resource parameters
58
62
  // This refreshes the parameters that configure the resource itself
59
- const keysToRefresh = new Set(Object.keys(resourceParameters));
60
- const currentParameters = await this.refresh(keysToRefresh);
63
+ const entriesToRefresh = new Map(Object.entries(resourceParameters));
64
+ const currentParameters = await this.refresh(entriesToRefresh);
61
65
 
62
66
  // Short circuit here. If resource is non-existent, then there's no point checking stateful parameters
63
67
  if (currentParameters == null) {
64
68
  return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
65
69
  }
66
70
 
67
- this.validateRefreshResults(currentParameters, keysToRefresh);
71
+ this.validateRefreshResults(currentParameters, entriesToRefresh);
68
72
 
69
73
  // Refresh stateful parameters
70
74
  // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
71
75
  for(const statefulParameter of statefulParameters) {
72
76
  const desiredValue = desiredParameters[statefulParameter.name];
73
77
 
74
- let currentValue = await statefulParameter.refresh() ?? undefined;
78
+ let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
75
79
 
76
80
  // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
77
81
  if (Array.isArray(currentValue)
@@ -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,15 +238,14 @@ 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
- private validateRefreshResults(refresh: Partial<T> | null, desiredKeys: Set<keyof T>) {
243
+ private validateRefreshResults(refresh: Partial<T> | null, desiredMap: Map<keyof T, T[keyof T]>) {
228
244
  if (!refresh) {
229
245
  return;
230
246
  }
231
247
 
248
+ const desiredKeys = new Set<keyof T>(desiredMap.keys());
232
249
  const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
233
250
 
234
251
  if (!setsEqual(desiredKeys, refreshKeys)) {
@@ -241,9 +258,19 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
241
258
  }
242
259
  }
243
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
+
244
271
  abstract validate(parameters: unknown): Promise<ValidationResult>;
245
272
 
246
- abstract refresh(keys: Set<keyof T>): Promise<Partial<T> | null>;
273
+ abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
247
274
 
248
275
  abstract applyCreate(plan: Plan<T>): Promise<void>;
249
276
 
@@ -32,7 +32,7 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
32
32
  this.configuration = configuration
33
33
  }
34
34
 
35
- abstract refresh(): Promise<V | null>;
35
+ abstract refresh(desired: V | null): Promise<V | null>;
36
36
 
37
37
  // TODO: Add an additional parameter here for what has actually changed.
38
38
  abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
@@ -88,7 +88,7 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
88
88
  }
89
89
  }
90
90
 
91
- abstract refresh(): Promise<V[] | null>;
91
+ abstract refresh(desired: V[] | null): Promise<V[] | null>;
92
92
  abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
93
93
  abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
94
94
  }