codify-plugin-lib 1.0.99 → 1.0.100

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.
package/dist/plan/plan.js CHANGED
@@ -126,7 +126,7 @@ export class Plan {
126
126
  }
127
127
  // TODO: Add object handling here in addition to arrays in the future
128
128
  const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
129
- .filter(([k, v]) => isArrayStatefulParameter(k, v))
129
+ .filter(([k, v]) => isArrayParameterWithFiltering(k, v))
130
130
  .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
131
131
  return { ...filteredCurrent, ...arrayStatefulParameters };
132
132
  function filterCurrent() {
@@ -143,18 +143,23 @@ export class Plan {
143
143
  return Object.fromEntries(Object.entries(current)
144
144
  .filter(([k]) => keys.has(k)));
145
145
  }
146
- function isArrayStatefulParameter(k, v) {
147
- return settings.parameterSettings?.[k]?.type === 'stateful'
148
- && settings.parameterSettings[k].definition.getSettings().type === 'array'
146
+ function isArrayParameterWithFiltering(k, v) {
147
+ return (((settings.parameterSettings?.[k]?.type === 'stateful'
148
+ && settings.parameterSettings[k].definition.getSettings().type === 'array')
149
+ && (settings.parameterSettings[k].definition.getSettings().filterInStatelessMode ?? true)) || (settings.parameterSettings?.[k]?.type === 'array'
150
+ && ((settings.parameterSettings?.[k]).filterInStatelessMode ?? true)))
149
151
  && Array.isArray(v);
150
152
  }
151
153
  // For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
152
154
  function filterArrayStatefulParameter(k, v) {
153
155
  const desiredArray = desired[k];
154
- const matcher = settings.parameterSettings[k]
155
- .definition
156
- .getSettings()
157
- .isElementEqual ?? ((a, b) => a === b);
156
+ const matcher = settings.parameterSettings[k].type === 'stateful'
157
+ ? settings.parameterSettings[k]
158
+ .definition
159
+ .getSettings()
160
+ .isElementEqual ?? ((a, b) => a === b)
161
+ : settings.parameterSettings[k]
162
+ .isElementEqual ?? ((a, b) => a === b);
158
163
  const desiredCopy = [...desiredArray];
159
164
  const currentCopy = [...v];
160
165
  const result = [];
@@ -52,9 +52,53 @@ export interface ResourceSettings<T extends StringIndexedObject> {
52
52
  * @param desired
53
53
  */
54
54
  inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
55
+ /**
56
+ * Customize the import behavior of the resource. By default, <code>codify import</code> will call `refresh()` with
57
+ * every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
58
+ * in the schema and will prompt the user for these values before performing the import.
59
+ *
60
+ * <b>Example:</b><br>
61
+ * Resource `alias` with parameters
62
+ *
63
+ * ```
64
+ * { alias <b>(*required)</b>: string; value: string; }
65
+ * ```
66
+ *
67
+ * When the user calls `codify import alias`, they will first be prompted to enter the value for `alias`. Refresh
68
+ * is then called with `refresh({ alias: 'user-input', value: null })`. The result returned to the user will then be:
69
+ *
70
+ * ```
71
+ * { type: 'alias', alias: 'user-input', value: 'git push' }
72
+ * ```
73
+ */
55
74
  import?: {
75
+ /**
76
+ * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
77
+ * from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in
78
+ * the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`).
79
+ * <br>
80
+ * The user will be prompted for the required parameters before the import starts. This is done because for most resources
81
+ * the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
82
+ * chooses which alias the resource is managing).
83
+ *
84
+ * See {@link import} for more information on how importing works.
85
+ */
56
86
  requiredParameters?: Array<Partial<keyof T>>;
87
+ /**
88
+ * Customize which keys will be refreshed in the import. Typically, `refresh()` statements only refresh
89
+ * the parameters provided as the input. Use `refreshKeys` to control which parameter keys are passed in.
90
+ * <br>
91
+ * By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
92
+ * in value can be customized using {@link defaultRefreshValues}
93
+ *
94
+ * See {@link import} for more information on how importing works.
95
+ */
57
96
  refreshKeys?: Array<Partial<keyof T>>;
97
+ /**
98
+ * Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
99
+ *
100
+ * See {@link import} for more information on how importing works.
101
+ */
58
102
  defaultRefreshValues?: Partial<T>;
59
103
  };
60
104
  }
@@ -129,6 +173,25 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
129
173
  * @return Return true if desired is equivalent to current.
130
174
  */
131
175
  isElementEqual?: (desired: any, current: any) => boolean;
176
+ /**
177
+ * Filter the contents of the refreshed array by the desired. This way items currently on the system but not
178
+ * in desired don't show up in the plan.
179
+ *
180
+ * <b>For example, for the nvm resource:</b>
181
+ * <ul>
182
+ * <li>Desired (20.18.0, 18.9.0, 16.3.1)</li>
183
+ * <li>Current (20.18.0, 22.1.3, 12.1.0)</li>
184
+ * </ul>
185
+ *
186
+ * Without filtering the plan will be:
187
+ * (~20.18.0, +18.9.0, +16.3.1, -22.1.3, -12.1.0)<br>
188
+ * With filtering the plan is: (~20.18.0, +18.9.0, +16.3.1)
189
+ *
190
+ * As you can see, filtering prevents items currently installed on the system from being removed.
191
+ *
192
+ * Defaults to true.
193
+ */
194
+ filterInStatelessMode?: boolean;
132
195
  }
133
196
  /**
134
197
  * Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.99",
3
+ "version": "1.0.100",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -147,6 +147,79 @@ describe('Plan entity tests', () => {
147
147
  operation: ResourceOperation.RECREATE
148
148
  })
149
149
  })
150
+
151
+ it('Filters array parameters in stateless mode (by default)', async () => {
152
+ const resource = new class extends TestResource {
153
+ getSettings(): ResourceSettings<any> {
154
+ return {
155
+ id: 'type',
156
+ parameterSettings: {
157
+ propZ: { type: 'array', isElementEqual: (a, b) => b.includes(a) }
158
+ }
159
+ }
160
+ }
161
+
162
+ async refresh(): Promise<Partial<any> | null> {
163
+ return {
164
+ propZ: [
165
+ '20.15.0',
166
+ '20.15.1'
167
+ ]
168
+ }
169
+ }
170
+ }
171
+
172
+ const controller = new ResourceController(resource);
173
+ const plan = await controller.plan({
174
+ propZ: ['20.15'],
175
+ } as any)
176
+
177
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
178
+ })
179
+
180
+ it('Doesn\'t filters array parameters if filtering is disabled', async () => {
181
+ const resource = new class extends TestResource {
182
+ getSettings(): ResourceSettings<any> {
183
+ return {
184
+ id: 'type',
185
+ parameterSettings: {
186
+ propZ: { type: 'array', canModify: true, isElementEqual: (a, b) => b.includes(a), filterInStatelessMode: false }
187
+ }
188
+ }
189
+ }
190
+
191
+ async refresh(): Promise<Partial<any> | null> {
192
+ return {
193
+ propZ: [
194
+ '20.15.0',
195
+ '20.15.1'
196
+ ]
197
+ }
198
+ }
199
+ }
200
+
201
+ const controller = new ResourceController(resource);
202
+ const plan = await controller.plan({
203
+ propZ: ['20.15'],
204
+ } as any)
205
+
206
+ expect(plan.changeSet).toMatchObject({
207
+ operation: ResourceOperation.MODIFY,
208
+ parameterChanges: expect.arrayContaining([
209
+ expect.objectContaining({
210
+ name: 'propZ',
211
+ previousValue: expect.arrayContaining([
212
+ '20.15.0',
213
+ '20.15.1'
214
+ ]),
215
+ newValue: expect.arrayContaining([
216
+ '20.15'
217
+ ]),
218
+ operation: 'modify'
219
+ })
220
+ ])
221
+ })
222
+ })
150
223
  })
151
224
 
152
225
  function createTestResource() {
package/src/plan/plan.ts CHANGED
@@ -220,7 +220,7 @@ export class Plan<T extends StringIndexedObject> {
220
220
  // TODO: Add object handling here in addition to arrays in the future
221
221
  const arrayStatefulParameters = Object.fromEntries(
222
222
  Object.entries(filteredCurrent)
223
- .filter(([k, v]) => isArrayStatefulParameter(k, v))
223
+ .filter(([k, v]) => isArrayParameterWithFiltering(k, v))
224
224
  .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
225
225
  )
226
226
 
@@ -247,19 +247,27 @@ export class Plan<T extends StringIndexedObject> {
247
247
  ) as Partial<T>;
248
248
  }
249
249
 
250
- function isArrayStatefulParameter(k: string, v: T[keyof T]): boolean {
251
- return settings.parameterSettings?.[k]?.type === 'stateful'
252
- && (settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings().type === 'array'
250
+ function isArrayParameterWithFiltering(k: string, v: T[keyof T]): boolean {
251
+ return (((settings.parameterSettings?.[k]?.type === 'stateful'
252
+ && (settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings().type === 'array')
253
+ && (((settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings() as ArrayParameterSetting).filterInStatelessMode ?? true)
254
+ ) || (
255
+ settings.parameterSettings?.[k]?.type === 'array'
256
+ && ((settings.parameterSettings?.[k] as ArrayParameterSetting).filterInStatelessMode ?? true)
257
+ ))
253
258
  && Array.isArray(v)
254
259
  }
255
260
 
256
261
  // For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
257
262
  function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
258
263
  const desiredArray = desired![k] as unknown[];
259
- const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
260
- .definition
261
- .getSettings() as ArrayParameterSetting)
262
- .isElementEqual ?? ((a, b) => a === b);
264
+ const matcher = settings.parameterSettings![k]!.type === 'stateful'
265
+ ? ((settings.parameterSettings![k] as StatefulParameterSetting)
266
+ .definition
267
+ .getSettings() as ArrayParameterSetting)
268
+ .isElementEqual ?? ((a, b) => a === b)
269
+ : (settings.parameterSettings![k] as ArrayParameterSetting)
270
+ .isElementEqual ?? ((a, b) => a === b)
263
271
 
264
272
  const desiredCopy = [...desiredArray];
265
273
  const currentCopy = [...v];
@@ -171,9 +171,9 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
171
171
  const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.import;
172
172
 
173
173
  const requiredParametersNotInSchema = requiredParameters
174
- ?.filter(
175
- (p) => schema && !(schema.properties[p])
176
- )
174
+ ?.filter(
175
+ (p) => schema && !(schema.properties[p])
176
+ )
177
177
  if (schema && requiredParametersNotInSchema && requiredParametersNotInSchema.length > 0) {
178
178
  throw new Error(`The following properties were declared in settings.import.requiredParameters but were not found in the schema:
179
179
  ${JSON.stringify(requiredParametersNotInSchema, null, 2)}`)
@@ -65,11 +65,56 @@ export interface ResourceSettings<T extends StringIndexedObject> {
65
65
  */
66
66
  inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
67
67
 
68
+ /**
69
+ * Customize the import behavior of the resource. By default, <code>codify import</code> will call `refresh()` with
70
+ * every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
71
+ * in the schema and will prompt the user for these values before performing the import.
72
+ *
73
+ * <b>Example:</b><br>
74
+ * Resource `alias` with parameters
75
+ *
76
+ * ```
77
+ * { alias <b>(*required)</b>: string; value: string; }
78
+ * ```
79
+ *
80
+ * When the user calls `codify import alias`, they will first be prompted to enter the value for `alias`. Refresh
81
+ * is then called with `refresh({ alias: 'user-input', value: null })`. The result returned to the user will then be:
82
+ *
83
+ * ```
84
+ * { type: 'alias', alias: 'user-input', value: 'git push' }
85
+ * ```
86
+ */
68
87
  import?: {
88
+
89
+ /**
90
+ * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
91
+ * from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in
92
+ * the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`).
93
+ * <br>
94
+ * The user will be prompted for the required parameters before the import starts. This is done because for most resources
95
+ * the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
96
+ * chooses which alias the resource is managing).
97
+ *
98
+ * See {@link import} for more information on how importing works.
99
+ */
69
100
  requiredParameters?: Array<Partial<keyof T>>;
70
101
 
102
+ /**
103
+ * Customize which keys will be refreshed in the import. Typically, `refresh()` statements only refresh
104
+ * the parameters provided as the input. Use `refreshKeys` to control which parameter keys are passed in.
105
+ * <br>
106
+ * By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
107
+ * in value can be customized using {@link defaultRefreshValues}
108
+ *
109
+ * See {@link import} for more information on how importing works.
110
+ */
71
111
  refreshKeys?: Array<Partial<keyof T>>;
72
112
 
113
+ /**
114
+ * Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
115
+ *
116
+ * See {@link import} for more information on how importing works.
117
+ */
73
118
  defaultRefreshValues?: Partial<T>
74
119
  }
75
120
  }
@@ -166,6 +211,26 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
166
211
  * @return Return true if desired is equivalent to current.
167
212
  */
168
213
  isElementEqual?: (desired: any, current: any) => boolean
214
+
215
+ /**
216
+ * Filter the contents of the refreshed array by the desired. This way items currently on the system but not
217
+ * in desired don't show up in the plan.
218
+ *
219
+ * <b>For example, for the nvm resource:</b>
220
+ * <ul>
221
+ * <li>Desired (20.18.0, 18.9.0, 16.3.1)</li>
222
+ * <li>Current (20.18.0, 22.1.3, 12.1.0)</li>
223
+ * </ul>
224
+ *
225
+ * Without filtering the plan will be:
226
+ * (~20.18.0, +18.9.0, +16.3.1, -22.1.3, -12.1.0)<br>
227
+ * With filtering the plan is: (~20.18.0, +18.9.0, +16.3.1)
228
+ *
229
+ * As you can see, filtering prevents items currently installed on the system from being removed.
230
+ *
231
+ * Defaults to true.
232
+ */
233
+ filterInStatelessMode?: boolean,
169
234
  }
170
235
 
171
236
  /**
@@ -151,8 +151,6 @@ describe('Stateful parameter tests', () => {
151
151
  nodeVersions: ['20.15'],
152
152
  } as any)
153
153
 
154
- console.log(JSON.stringify(plan, null, 2))
155
-
156
154
  expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
157
155
  })
158
156
  })