codify-plugin-lib 1.0.154 → 1.0.156

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.
@@ -104,6 +104,11 @@ export class ResourceController {
104
104
  }
105
105
  async plan(core, desired, state, isStateful = false) {
106
106
  this.validatePlanInputs(core, desired, state, isStateful);
107
+ const context = {
108
+ commandType: 'plan',
109
+ isStateful,
110
+ originalDesiredConfig: structuredClone(desired),
111
+ };
107
112
  this.addDefaultValues(desired);
108
113
  await this.applyTransformParameters(desired);
109
114
  this.addDefaultValues(state);
@@ -112,7 +117,7 @@ export class ResourceController {
112
117
  const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters);
113
118
  const { allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
114
119
  // Refresh resource parameters. This refreshes the parameters that configure the resource itself
115
- const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
120
+ const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
116
121
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
117
122
  if (currentArray === null
118
123
  || currentArray === undefined
@@ -180,6 +185,11 @@ export class ResourceController {
180
185
  if (this.settings.importAndDestroy?.preventImport) {
181
186
  throw new Error(`Type: ${this.typeId} cannot be imported`);
182
187
  }
188
+ const context = {
189
+ commandType: 'import',
190
+ isStateful: true,
191
+ originalDesiredConfig: structuredClone(parameters),
192
+ };
183
193
  this.addDefaultValues(parameters);
184
194
  await this.applyTransformParameters(parameters);
185
195
  // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
@@ -188,6 +198,11 @@ export class ResourceController {
188
198
  ...Object.fromEntries(this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null])),
189
199
  ...this.settings.importAndDestroy?.defaultRefreshValues,
190
200
  ...parameters,
201
+ ...(Object.fromEntries(// If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead
202
+ Object.entries(parameters).filter(([k, v]) => this.parsedSettings.defaultValues[k] !== undefined
203
+ && v === this.parsedSettings.defaultValues[k]
204
+ && context.originalDesiredConfig?.[k] === undefined
205
+ && this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined).map(([k]) => [k, this.settings.importAndDestroy.defaultRefreshValues[k]])))
191
206
  }
192
207
  : {
193
208
  ...Object.fromEntries(this.getAllParameterKeys().map((k) => [k, null])),
@@ -197,7 +212,7 @@ export class ResourceController {
197
212
  // Parse data from the user supplied config
198
213
  const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters);
199
214
  const { allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
200
- const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
215
+ const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
201
216
  if (currentParametersArray === null
202
217
  || currentParametersArray === undefined
203
218
  || currentParametersArray.filter(Boolean).length === 0) {
@@ -312,8 +327,8 @@ ${JSON.stringify(refresh, null, 2)}
312
327
  }
313
328
  }
314
329
  }
315
- async refreshNonStatefulParameters(resourceParameters) {
316
- const result = await this.resource.refresh(resourceParameters);
330
+ async refreshNonStatefulParameters(resourceParameters, context) {
331
+ const result = await this.resource.refresh(resourceParameters, context);
317
332
  const currentParametersArray = Array.isArray(result) || result === null
318
333
  ? result
319
334
  : [result];
@@ -2,6 +2,11 @@ import { StringIndexedObject } from 'codify-schemas';
2
2
  import { ParameterChange } from '../plan/change-set.js';
3
3
  import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
4
4
  import { ResourceSettings } from './resource-settings.js';
5
+ export interface RefreshContext<T extends StringIndexedObject> {
6
+ isStateful: boolean;
7
+ commandType: 'destroy' | 'import' | 'plan';
8
+ originalDesiredConfig: Partial<T> | null;
9
+ }
5
10
  /**
6
11
  * A resource represents an object on the system (application, CLI tool, or setting)
7
12
  * that has state and can be created and destroyed. Examples of resources include CLI tools
@@ -65,10 +70,12 @@ export declare abstract class Resource<T extends StringIndexedObject> {
65
70
  * of the desired config. In stateful mode, this will be parameters of the state config + the desired
66
71
  * config of any new parameters.
67
72
  *
73
+ * @param context Context surrounding the request
74
+ *
68
75
  * @return A config or an array of configs representing the status of the resource on the
69
76
  * system currently
70
77
  */
71
- abstract refresh(parameters: Partial<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
78
+ abstract refresh(parameters: Partial<T>, context: RefreshContext<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
72
79
  /**
73
80
  * Create the resource (install) based on the parameters passed in. Only the desired parameters will
74
81
  * be non-null because in a CREATE plan, the current value is null.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.154",
3
+ "version": "1.0.156",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -746,4 +746,88 @@ describe('Resource tests', () => {
746
746
  }
747
747
  })
748
748
  })
749
+
750
+ it('Can plan with settings', async () => {
751
+ const resource = new class extends TestResource {
752
+ getSettings(): ResourceSettings<any> {
753
+ return {
754
+ id: 'path',
755
+ parameterSettings: {
756
+ path: { type: 'string', isEqual: 'directory' },
757
+ paths: { canModify: true, type: 'array', isElementEqual: 'directory' },
758
+ prepend: { default: false, setting: true },
759
+ declarationsOnly: { default: false, setting: true },
760
+ },
761
+ importAndDestroy: {
762
+ refreshKeys: ['paths', 'declarationsOnly'],
763
+ defaultRefreshValues: {
764
+ paths: [],
765
+ declarationsOnly: true,
766
+ }
767
+ },
768
+ allowMultiple: {
769
+ matcher: (desired, current) => {
770
+ if (desired.path) {
771
+ return desired.path === current.path;
772
+ }
773
+
774
+ const currentPaths = new Set(current.paths)
775
+ return desired.paths?.some((p) => currentPaths.has(p));
776
+ }
777
+ }
778
+ }
779
+ }
780
+
781
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
782
+ return { path: '$HOME/.bun/bin', prepend: false, declarationsOnly: false }
783
+ }
784
+ }
785
+
786
+ const controller = new ResourceController(resource);
787
+ const plan = await controller.plan({ type: 'path' }, { path: '$HOME/.bun/bin' }, null, false);
788
+
789
+ expect(plan.requiresChanges()).to.be.false;
790
+ })
791
+
792
+ it('Can import with the correct default parameters', async () => {
793
+ const resource = new class extends TestResource {
794
+ getSettings(): ResourceSettings<any> {
795
+ return {
796
+ id: 'path',
797
+ parameterSettings: {
798
+ path: { type: 'string', isEqual: 'directory' },
799
+ paths: { canModify: true, type: 'array', isElementEqual: 'directory' },
800
+ prepend: { default: false, setting: true },
801
+ declarationsOnly: { default: false, setting: true },
802
+ },
803
+ importAndDestroy: {
804
+ refreshKeys: ['paths', 'declarationsOnly'],
805
+ defaultRefreshValues: {
806
+ paths: [],
807
+ declarationsOnly: true,
808
+ }
809
+ },
810
+ allowMultiple: {
811
+ matcher: (desired, current) => {
812
+ if (desired.path) {
813
+ return desired.path === current.path;
814
+ }
815
+
816
+ const currentPaths = new Set(current.paths)
817
+ return desired.paths?.some((p) => currentPaths.has(p));
818
+ }
819
+ }
820
+ }
821
+ }
822
+
823
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
824
+ expect(parameters.declarationsOnly).to.be.true;
825
+
826
+ return null;
827
+ }
828
+ }
829
+
830
+ const controller = new ResourceController(resource);
831
+ const plan = await controller.import({ type: 'path' }, {});
832
+ })
749
833
  });
@@ -13,7 +13,7 @@ import { Plan } from '../plan/plan.js';
13
13
  import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
14
14
  import { ConfigParser } from './config-parser.js';
15
15
  import { ParsedResourceSettings } from './parsed-resource-settings.js';
16
- import { Resource } from './resource.js';
16
+ import { RefreshContext, Resource } from './resource.js';
17
17
  import { ResourceSettings } from './resource-settings.js';
18
18
 
19
19
  export class ResourceController<T extends StringIndexedObject> {
@@ -151,6 +151,11 @@ export class ResourceController<T extends StringIndexedObject> {
151
151
  isStateful = false,
152
152
  ): Promise<Plan<T>> {
153
153
  this.validatePlanInputs(core, desired, state, isStateful);
154
+ const context: RefreshContext<T> = {
155
+ commandType: 'plan',
156
+ isStateful,
157
+ originalDesiredConfig: structuredClone(desired),
158
+ };
154
159
 
155
160
  this.addDefaultValues(desired);
156
161
  await this.applyTransformParameters(desired);
@@ -167,7 +172,7 @@ export class ResourceController<T extends StringIndexedObject> {
167
172
  } = parsedConfig;
168
173
 
169
174
  // Refresh resource parameters. This refreshes the parameters that configure the resource itself
170
- const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
175
+ const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
171
176
 
172
177
  // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
173
178
  if (currentArray === null
@@ -259,6 +264,12 @@ export class ResourceController<T extends StringIndexedObject> {
259
264
  throw new Error(`Type: ${this.typeId} cannot be imported`);
260
265
  }
261
266
 
267
+ const context: RefreshContext<T> = {
268
+ commandType: 'import',
269
+ isStateful: true,
270
+ originalDesiredConfig: structuredClone(parameters),
271
+ };
272
+
262
273
  this.addDefaultValues(parameters);
263
274
  await this.applyTransformParameters(parameters);
264
275
 
@@ -270,6 +281,14 @@ export class ResourceController<T extends StringIndexedObject> {
270
281
  ),
271
282
  ...this.settings.importAndDestroy?.defaultRefreshValues,
272
283
  ...parameters,
284
+ ...(Object.fromEntries( // If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead
285
+ Object.entries(parameters).filter(([k, v]) =>
286
+ this.parsedSettings.defaultValues[k] !== undefined
287
+ && v === this.parsedSettings.defaultValues[k]
288
+ && context.originalDesiredConfig?.[k] === undefined
289
+ && this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined
290
+ ).map(([k]) => [k, this.settings.importAndDestroy!.defaultRefreshValues![k]])
291
+ ))
273
292
  }
274
293
  : {
275
294
  ...Object.fromEntries(
@@ -287,7 +306,7 @@ export class ResourceController<T extends StringIndexedObject> {
287
306
  allStatefulParameters,
288
307
  } = parsedConfig;
289
308
 
290
- const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
309
+ const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context);
291
310
 
292
311
  if (currentParametersArray === null
293
312
  || currentParametersArray === undefined
@@ -434,8 +453,8 @@ ${JSON.stringify(refresh, null, 2)}
434
453
 
435
454
  }
436
455
 
437
- private async refreshNonStatefulParameters(resourceParameters: Partial<T>): Promise<Array<Partial<T>> | null> {
438
- const result = await this.resource.refresh(resourceParameters);
456
+ private async refreshNonStatefulParameters(resourceParameters: Partial<T>, context: RefreshContext<T>): Promise<Array<Partial<T>> | null> {
457
+ const result = await this.resource.refresh(resourceParameters, context);
439
458
 
440
459
  const currentParametersArray = Array.isArray(result) || result === null
441
460
  ? result
@@ -4,6 +4,12 @@ import { ParameterChange } from '../plan/change-set.js';
4
4
  import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
5
5
  import { ResourceSettings } from './resource-settings.js';
6
6
 
7
+ export interface RefreshContext<T extends StringIndexedObject> {
8
+ isStateful: boolean;
9
+ commandType: 'destroy' | 'import' | 'plan';
10
+ originalDesiredConfig: Partial<T> | null;
11
+ }
12
+
7
13
  /**
8
14
  * A resource represents an object on the system (application, CLI tool, or setting)
9
15
  * that has state and can be created and destroyed. Examples of resources include CLI tools
@@ -73,10 +79,12 @@ export abstract class Resource<T extends StringIndexedObject> {
73
79
  * of the desired config. In stateful mode, this will be parameters of the state config + the desired
74
80
  * config of any new parameters.
75
81
  *
82
+ * @param context Context surrounding the request
83
+ *
76
84
  * @return A config or an array of configs representing the status of the resource on the
77
85
  * system currently
78
86
  */
79
- abstract refresh(parameters: Partial<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
87
+ abstract refresh(parameters: Partial<T>, context: RefreshContext<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
80
88
 
81
89
  /**
82
90
  * Create the resource (install) based on the parameters passed in. Only the desired parameters will