codify-plugin-lib 1.0.76 → 1.0.77

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.
Files changed (70) hide show
  1. package/.eslintrc.json +11 -4
  2. package/.github/workflows/release.yaml +19 -0
  3. package/.github/workflows/unit-test-ci.yaml +19 -0
  4. package/dist/errors.d.ts +4 -0
  5. package/dist/errors.js +7 -0
  6. package/dist/index.d.ts +10 -10
  7. package/dist/index.js +9 -9
  8. package/dist/messages/handlers.d.ts +1 -1
  9. package/dist/messages/handlers.js +2 -1
  10. package/dist/plan/change-set.d.ts +37 -0
  11. package/dist/plan/change-set.js +146 -0
  12. package/dist/plan/plan-types.d.ts +23 -0
  13. package/dist/plan/plan-types.js +1 -0
  14. package/dist/plan/plan.d.ts +59 -0
  15. package/dist/plan/plan.js +228 -0
  16. package/dist/plugin/plugin.d.ts +17 -0
  17. package/dist/plugin/plugin.js +83 -0
  18. package/dist/resource/config-parser.d.ts +14 -0
  19. package/dist/resource/config-parser.js +48 -0
  20. package/dist/resource/parsed-resource-settings.d.ts +26 -0
  21. package/dist/resource/parsed-resource-settings.js +126 -0
  22. package/dist/resource/resource-controller.d.ts +30 -0
  23. package/dist/resource/resource-controller.js +247 -0
  24. package/dist/resource/resource-settings.d.ts +149 -0
  25. package/dist/resource/resource-settings.js +9 -0
  26. package/dist/resource/resource.d.ts +137 -0
  27. package/dist/resource/resource.js +44 -0
  28. package/dist/resource/stateful-parameter.d.ts +164 -0
  29. package/dist/resource/stateful-parameter.js +94 -0
  30. package/dist/utils/utils.d.ts +19 -3
  31. package/dist/utils/utils.js +52 -3
  32. package/package.json +5 -3
  33. package/src/index.ts +10 -11
  34. package/src/messages/handlers.test.ts +10 -37
  35. package/src/messages/handlers.ts +2 -2
  36. package/src/plan/change-set.test.ts +220 -0
  37. package/src/plan/change-set.ts +225 -0
  38. package/src/plan/plan-types.ts +27 -0
  39. package/src/{entities → plan}/plan.test.ts +35 -29
  40. package/src/plan/plan.ts +353 -0
  41. package/src/{entities → plugin}/plugin.test.ts +14 -13
  42. package/src/{entities → plugin}/plugin.ts +28 -24
  43. package/src/resource/config-parser.ts +77 -0
  44. package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
  45. package/src/resource/parsed-resource-settings.ts +179 -0
  46. package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
  47. package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
  48. package/src/resource/resource-controller.ts +340 -0
  49. package/src/resource/resource-settings.test.ts +494 -0
  50. package/src/resource/resource-settings.ts +192 -0
  51. package/src/resource/resource.ts +149 -0
  52. package/src/resource/stateful-parameter.test.ts +93 -0
  53. package/src/resource/stateful-parameter.ts +217 -0
  54. package/src/utils/test-utils.test.ts +87 -0
  55. package/src/utils/utils.test.ts +2 -2
  56. package/src/utils/utils.ts +51 -5
  57. package/tsconfig.json +0 -1
  58. package/vitest.config.ts +10 -0
  59. package/src/entities/change-set.test.ts +0 -155
  60. package/src/entities/change-set.ts +0 -244
  61. package/src/entities/plan-types.ts +0 -44
  62. package/src/entities/plan.ts +0 -178
  63. package/src/entities/resource-options.ts +0 -155
  64. package/src/entities/resource-parameters.test.ts +0 -604
  65. package/src/entities/resource-types.ts +0 -31
  66. package/src/entities/resource.ts +0 -470
  67. package/src/entities/stateful-parameter.test.ts +0 -114
  68. package/src/entities/stateful-parameter.ts +0 -92
  69. package/src/entities/transform-parameter.ts +0 -13
  70. /package/src/{entities/errors.ts → errors.ts} +0 -0
@@ -0,0 +1,149 @@
1
+ import { StringIndexedObject, } from 'codify-schemas';
2
+
3
+ import { ParameterChange } from '../plan/change-set.js';
4
+ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
5
+ import { ResourceSettings } from './resource-settings.js';
6
+
7
+ /**
8
+ * A resource represents an object on the system (application, CLI tool, or setting)
9
+ * that has state and can be created and destroyed. Examples of resources include CLI tools
10
+ * like homebrew, docker, and xcode-tools; applications like Google Chrome, Zoom, and OpenVPN;
11
+ * and settings like AWS profiles, git configs and system preference settings.
12
+ */
13
+ export abstract class Resource<T extends StringIndexedObject> {
14
+
15
+ /**
16
+ * Return the settings for the resource. Consult the typing for {@link ResourceSettings} for
17
+ * a description of the options.
18
+ *
19
+ * **Parameters**:
20
+ * - id: The id of the resource. This translates to the `type` id parameter in codify.json configs
21
+ * - schema: A JSON schema used to validate user input
22
+ * - allowMultiple: Allow multiple copies of the resource to exist at the same time. If true then,
23
+ * a matcher must be defined that matches a user defined config and a single resource on the system.
24
+ * - removeStatefulParametersBeforeDestory: Call the delete methods of stateful parameters before destorying
25
+ * the base resource. Defaults to false.
26
+ * - dependencies: Specify the ids of any resources that this resource depends on
27
+ * - parameterSettings: Parameter specific settings. Use this to define custom equals functions, default values
28
+ * and input transformations
29
+ * - inputTransformation: Transform the input value.
30
+ *
31
+ * @return ResourceSettings The resource settings
32
+ */
33
+ abstract getSettings(): ResourceSettings<T>;
34
+
35
+ async initialize(): Promise<void> {
36
+ };
37
+
38
+ /**
39
+ * Add custom validation logic in-addition to the default schema validation.
40
+ * In this method throw an error if the object did not validate. The message of the
41
+ * error will be shown to the user.
42
+ * @param parameters
43
+ */
44
+ async validate(parameters: Partial<T>): Promise<void> {
45
+ };
46
+
47
+ /**
48
+ * Return the status of the resource on the system. If multiple resources exist, then return all instances of
49
+ * the resource back. Query for the individual parameters specified in the parameter param.
50
+ * Return null if the resource does not exist.
51
+ *
52
+ * Example (Android Studios Resource):
53
+ * 1. Receive Input:
54
+ * ```
55
+ * {
56
+ * name: 'Android Studios.app'
57
+ * directory: '/Application',
58
+ * version: '2023.2'
59
+ * }
60
+ * ```
61
+ * 2. Query the system for any installed Android studio versions.
62
+ * 3. In this example we find that there is an 2023.2 version installed and an
63
+ * additional 2024.3-beta version installed as well.
64
+ * 4. We would return:
65
+ * ```
66
+ * [
67
+ * { name: 'Android Studios.app', directory: '/Application', version: '2023.2' },
68
+ * { name: 'Android Studios Preview.app', directory: '/Application', version: '2024.3' },
69
+ * ]
70
+ * ```
71
+ *
72
+ * @param parameters The parameters to refresh. In stateless mode this will be the parameters
73
+ * of the desired config. In stateful mode, this will be parameters of the state config + the desired
74
+ * config of any new parameters.
75
+ *
76
+ * @return A config or an array of configs representing the status of the resource on the
77
+ * system currently
78
+ */
79
+ abstract refresh(parameters: Partial<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
80
+
81
+ /**
82
+ * Create the resource (install) based on the parameters passed in. Only the desired parameters will
83
+ * be non-null because in a CREATE plan, the current value is null.
84
+ *
85
+ * Example (Android Studios Resource):
86
+ * 1. We receive a plan of:
87
+ * ```
88
+ * Plan {
89
+ * desiredConfig: {
90
+ * name: 'Android Studios.app',
91
+ * directory: '/Application',
92
+ * version: '2023.2'
93
+ * }
94
+ * currentConfig: null,
95
+ * }
96
+ * ```
97
+ * 2. Install version Android Studios 2023.2 and then return.
98
+ *
99
+ * @param plan The plan of what to install. Use only the desiredConfig because currentConfig is null.
100
+ */
101
+ abstract create(plan: CreatePlan<T>): Promise<void>;
102
+
103
+ /**
104
+ * Modify a single parameter of a resource. Modify is optional to override and is only called
105
+ * when a resourceSetting was set to `canModify = true`. This method should only modify
106
+ * a single parameter at a time as specified by the first parameter: ParameterChange.
107
+ *
108
+ * Example (AWS Profile Resource):
109
+ * 1. We receive a parameter change of:
110
+ * ```
111
+ * {
112
+ * name: 'awsAccessKeyId',
113
+ * operation: ParameterOperation.MODIFY,
114
+ * newValue: '123456',
115
+ * previousValue: 'abcdef'
116
+ * }
117
+ * ```
118
+ * 2. Use an if statement to only apply this operation for the parameter `awsAccessKeyId`
119
+ * 3. Update the value of the `aws_access_key_id` to the `newValue` specified in the parameter change
120
+ *
121
+ * @param pc ParameterChange, the parameter name and values to modify on the resource
122
+ * @param plan The overall plan that triggered the modify operation
123
+ */
124
+ async modify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void> {
125
+ };
126
+
127
+ /**
128
+ * Destroy the resource (uninstall) based on the parameters passed in. Only the current parameters will
129
+ * be non-null because in a DESTROY plan, the desired value is null. This method will only be called in
130
+ * stateful mode.
131
+ *
132
+ * Example (Android Studios Resource):
133
+ * 1. We receive a plan of:
134
+ * ```
135
+ * Plan {
136
+ * currentConfig: {
137
+ * name: 'Android Studios.app',
138
+ * directory: '/Application',
139
+ * version: '2022.4'
140
+ * },
141
+ * desiredConfig: null
142
+ * }
143
+ * ```
144
+ * 2. Uninstall version Android Studios 2022.4 and then return.
145
+ *
146
+ * @param plan The plan of what to uninstall. Use only the currentConfig because desiredConfig is null.
147
+ */
148
+ abstract destroy(plan: DestroyPlan<T>): Promise<void>;
149
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { spy } from 'sinon';
3
+ import { ParameterOperation, ResourceOperation } from 'codify-schemas';
4
+ import { TestArrayStatefulParameter, TestConfig, testPlan } from '../utils/test-utils.test.js';
5
+ import { ArrayParameterSetting } from './resource-settings.js';
6
+
7
+ describe('Stateful parameter tests', () => {
8
+ it('addItem is called the correct number of times', async () => {
9
+ const plan = testPlan<TestConfig>({
10
+ desired: { propZ: ['a', 'b', 'c'] },
11
+ });
12
+
13
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
14
+ expect(plan.changeSet.parameterChanges.length).to.eq(1);
15
+
16
+ const testParameter = spy(new TestArrayStatefulParameter());
17
+ await testParameter.add((plan.desiredConfig! as any).propZ, plan);
18
+
19
+ expect(testParameter.addItem.callCount).to.eq(3);
20
+ expect(testParameter.removeItem.called).to.be.false;
21
+ })
22
+
23
+ it('applyRemoveItem is called the correct number of times', async () => {
24
+ const plan = testPlan<TestConfig>({
25
+ desired: null,
26
+ current: [{ propZ: ['a', 'b', 'c'] }],
27
+ state: { propZ: ['a', 'b', 'c'] },
28
+ statefulMode: true,
29
+ });
30
+
31
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY);
32
+ expect(plan.changeSet.parameterChanges.length).to.eq(1);
33
+
34
+ const testParameter = spy(new TestArrayStatefulParameter());
35
+ await testParameter.remove((plan.currentConfig as any).propZ, plan);
36
+
37
+ expect(testParameter.addItem.called).to.be.false;
38
+ expect(testParameter.removeItem.callCount).to.eq(3);
39
+ })
40
+
41
+ it('In stateless mode only applyAddItem is called only for modifies', async () => {
42
+ const parameter = new TestArrayStatefulParameter()
43
+ const plan = testPlan<TestConfig>({
44
+ desired: { propZ: ['a', 'c', 'd', 'e', 'f'] }, // b to remove, d, e, f to add
45
+ current: [{ propZ: ['a', 'b', 'c'] }],
46
+ settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: parameter } } },
47
+ });
48
+
49
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
50
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
51
+ name: 'propZ',
52
+ previousValue: ['a', 'c'], // In stateless mode the previous value gets filtered to prevent deletes
53
+ newValue: ['a', 'c', 'd', 'e', 'f'],
54
+ operation: ParameterOperation.MODIFY,
55
+ })
56
+
57
+ const testParameter = spy(parameter);
58
+ await testParameter.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
59
+
60
+ expect(testParameter.addItem.calledThrice).to.be.true;
61
+ expect(testParameter.removeItem.called).to.be.false;
62
+ })
63
+
64
+ it('isElementEqual is called for modifies', async () => {
65
+ const testParameter = spy(new class extends TestArrayStatefulParameter {
66
+ getSettings(): ArrayParameterSetting {
67
+ return {
68
+ type: 'array',
69
+ isElementEqual: (desired, current) => current.includes(desired),
70
+ }
71
+ }
72
+ });
73
+
74
+ const plan = testPlan<TestConfig>({
75
+ desired: { propZ: ['9.12', '9.13'] }, // b to remove, d, e, f to add
76
+ current: [{ propZ: ['9.12.9'] }],
77
+ settings: { id: 'type', parameterSettings: { propZ: { type: 'stateful', definition: testParameter } } }
78
+ });
79
+
80
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.MODIFY);
81
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
82
+ name: 'propZ',
83
+ previousValue: ['9.12.9'],
84
+ newValue: ['9.12', '9.13'],
85
+ operation: ParameterOperation.MODIFY,
86
+ })
87
+
88
+ await testParameter.modify((plan.desiredConfig as any).propZ, (plan.currentConfig as any).propZ, plan);
89
+
90
+ expect(testParameter.addItem.calledOnce).to.be.true;
91
+ expect(testParameter.removeItem.called).to.be.false;
92
+ })
93
+ })
@@ -0,0 +1,217 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+
3
+ import { Plan } from '../plan/plan.js';
4
+ import { ArrayParameterSetting, ParameterSetting } from './resource-settings.js';
5
+
6
+ /**
7
+ * A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
8
+ * is still tied to the overall lifecycle of a resource.
9
+ *
10
+ * **Examples include:**
11
+ * 1. Homebrew formulas are stateful parameters. They can be installed and uninstalled but they are still tied to the
12
+ * overall lifecycle of homebrew
13
+ * 2. Nvm installed node versions are stateful parameters. Nvm can install and uninstall different versions of Node but
14
+ * these versions are tied to the lifecycle of nvm. If nvm is uninstalled then so are the Node versions.
15
+ */
16
+ export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
17
+
18
+ /**
19
+ * Parameter settings for the stateful parameter. Stateful parameters share the same parameter settings as
20
+ * regular parameters except that they cannot be of type 'stateful'. See {@link ParameterSetting} for more
21
+ * information on available settings.
22
+ *
23
+ * @return The parameter settings
24
+ */
25
+ getSettings(): ParameterSetting {
26
+ return {}
27
+ }
28
+
29
+ /**
30
+ * Refresh the status of the stateful parameter on the system. This method works similarly to {@link Resource.refresh}.
31
+ * Return the value of the stateful parameter or null if not found.
32
+ *
33
+ * @param desired The desired value of the user.
34
+ *
35
+ * @return The value of the stateful parameter currently on the system or null if not found
36
+ */
37
+ abstract refresh(desired: V | null): Promise<V | null>;
38
+
39
+ /**
40
+ * Create the stateful parameter on the system. This method is similar {@link Resource.create} except that its only
41
+ * applicable to the stateful parameter. For resource `CREATE` operations, this method will be called after the
42
+ * resource is successfully created. The add method is called when a ParameterChange is ADD in a plan. The add
43
+ * method is only called when the stateful parameter does not currently exist.
44
+ *
45
+ * **Example (Homebrew formula):**
46
+ * 1. Add is called with a value of:
47
+ * ```
48
+ * ['jq', 'jenv']
49
+ * ```
50
+ * 2. Add handles the request by calling `brew install --formulae jq jenv`
51
+ *
52
+ * @param valueToAdd The desired value of the stateful parameter.
53
+ * @param plan The overall plan that contains the ADD
54
+ */
55
+ abstract add(valueToAdd: V, plan: Plan<T>): Promise<void>;
56
+
57
+ /**
58
+ * Modify the state of a stateful parameter on the system. This method is similar to {@link Resource.modify} except that its only
59
+ * applicable to the stateful parameter.
60
+ *
61
+ * **Example (Git email parameter):**
62
+ * 1. Add is called with a value of:
63
+ * ```
64
+ * newValue: 'email+new@gmail.com', previousValue: 'email+old@gmail.com'
65
+ * ```
66
+ * 2. Modify handles the request by calling `git config --global user.email email+new@gmail.com`
67
+ *
68
+ * @param newValue The desired value of the stateful parameter
69
+ * @param previousValue The current value of the stateful parameter
70
+ * @param plan The overall plan
71
+ */
72
+ abstract modify(newValue: V, previousValue: V, plan: Plan<T>): Promise<void>;
73
+
74
+ /**
75
+ * Create the stateful parameter on the system. This method is similar {@link Resource.destroy} except that its only
76
+ * applicable to the stateful parameter. The remove method is only called when the stateful parameter already currently exist.
77
+ * This method corresponds to REMOVE parameter operations in a plan.
78
+ * For resource `DESTORY`, this method is only called if the {@link ResourceSettings.removeStatefulParametersBeforeDestroy}
79
+ * is set to true. This method will be called before the resource is destroyed.
80
+ *
81
+ * **Example (Homebrew formula):**
82
+ * 1. Remove is called with a value of:
83
+ * ```
84
+ * ['jq', 'jenv']
85
+ * ```
86
+ * 2. Remove handles the request by calling `brew uninstall --formulae jq jenv`
87
+ *
88
+ * @param valueToRemove The value to remove from the stateful parameter.
89
+ * @param plan The overall plan that contains the REMOVE
90
+ */
91
+ abstract remove(valueToRemove: V, plan: Plan<T>): Promise<void>;
92
+ }
93
+
94
+ /**
95
+ * A specialized version of {@link StatefulParameter } that is used for stateful parameters which are arrays.
96
+ * A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
97
+ * is still tied to the overall lifecycle of a resource.
98
+ *
99
+ * **Examples:**
100
+ * - Homebrew formulas are arrays
101
+ * - Pyenv python versions are arrays
102
+ */
103
+ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
104
+
105
+ /**
106
+ * Parameter level settings. Type must be 'array'.
107
+ */
108
+ getSettings(): ArrayParameterSetting {
109
+ return { type: 'array' }
110
+ }
111
+
112
+ /**
113
+ * It is not recommended to override the `add` method. A addItem helper method is available to operate on
114
+ * individual elements of the desired array. See {@link StatefulParameter.add} for more info.
115
+ *
116
+ * @param valuesToAdd The array of values to add
117
+ * @param plan The overall plan
118
+ *
119
+ */
120
+ async add(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
121
+ for (const value of valuesToAdd) {
122
+ await this.addItem(value, plan);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * It is not recommended to override the `modify` method. `addItem` and `removeItem` will be called accordingly based
128
+ * on the modifications. See {@link StatefulParameter.modify} for more info.
129
+ *
130
+ * @param newValues The new array value
131
+ * @param previousValues The previous array value
132
+ * @param plan The overall plan
133
+ */
134
+ async modify(newValues: V[], previousValues: V[], plan: Plan<T>): Promise<void> {
135
+
136
+ // TODO: I don't think this works with duplicate elements. Solve at another time
137
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
138
+ if (this.getSettings()?.isElementEqual) {
139
+ return this.getSettings().isElementEqual!(n, p);
140
+ }
141
+
142
+ return n === p;
143
+ }));
144
+
145
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
146
+ if (this.getSettings().isElementEqual) {
147
+ return this.getSettings().isElementEqual!(n, p);
148
+ }
149
+
150
+ return n === p;
151
+ }));
152
+
153
+ for (const value of valuesToAdd) {
154
+ await this.addItem(value, plan)
155
+ }
156
+
157
+ for (const value of valuesToRemove) {
158
+ await this.removeItem(value, plan)
159
+ }
160
+ }
161
+
162
+ /**
163
+ * It is not recommended to override the `remove` method. A removeItem helper method is available to operate on
164
+ * individual elements of the desired array. See {@link StatefulParameter.remove} for more info.
165
+ *
166
+ * @param valuesToAdd The array of values to add
167
+ * @param plan The overall plan
168
+ *
169
+ */
170
+ async remove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
171
+ for (const value of valuesToRemove) {
172
+ await this.removeItem(value as V, plan);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * See {@link StatefulParameter.refresh} for more info.
178
+ *
179
+ * @param desired The desired value to refresh
180
+ * @return The current value on the system or null if not found.
181
+ */
182
+ abstract refresh(desired: V[] | null): Promise<V[] | null>;
183
+
184
+ /**
185
+ * Helper method that gets called when individual elements of the array need to be added. See {@link StatefulParameter.add}
186
+ * for more information.
187
+ *
188
+ * Example (Homebrew formula):
189
+ * 1. The stateful parameter receives an input of:
190
+ * ```
191
+ * ['jq', 'jenv', 'docker']
192
+ * ```
193
+ * 2. Internally the stateful parameter will iterate the array and call `addItem` for each element
194
+ * 3. Override addItem and install each formula using `brew install --formula jq`
195
+ *
196
+ * @param item The item to add (install)
197
+ * @param plan The overall plan
198
+ */
199
+ abstract addItem(item: V, plan: Plan<T>): Promise<void>;
200
+
201
+ /**
202
+ * Helper method that gets called when individual elements of the array need to be removed. See {@link StatefulParameter.remove}
203
+ * for more information.
204
+ *
205
+ * Example (Homebrew formula):
206
+ * 1. The stateful parameter receives an input of:
207
+ * ```
208
+ * ['jq', 'jenv', 'docker']
209
+ * ```
210
+ * 2. Internally the stateful parameter will iterate the array and call `removeItem` for each element
211
+ * 3. Override removeItem and uninstall each formula using `brew uninstall --formula jq`
212
+ *
213
+ * @param item The item to remove (uninstall)
214
+ * @param plan The overall plan
215
+ */
216
+ abstract removeItem(item: V, plan: Plan<T>): Promise<void>;
217
+ }
@@ -0,0 +1,87 @@
1
+ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
2
+ import { ResourceSettings } from '../resource/resource-settings.js';
3
+ import { Plan } from '../plan/plan.js';
4
+ import { Resource } from '../resource/resource.js';
5
+ import { CreatePlan, DestroyPlan } from '../plan/plan-types.js';
6
+ import { ArrayStatefulParameter, StatefulParameter } from '../resource/stateful-parameter.js';
7
+ import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
8
+
9
+ export function testPlan<T extends StringIndexedObject>(params: {
10
+ desired?: Partial<T> | null;
11
+ current?: Partial<T>[] | null;
12
+ state?: Partial<T> | null;
13
+ core?: ResourceConfig;
14
+ settings?: ResourceSettings<T>;
15
+ statefulMode?: boolean;
16
+ }) {
17
+ return Plan.calculate({
18
+ desiredParameters: params.desired ?? null,
19
+ currentParametersArray: params.current ?? null,
20
+ stateParameters: params.state ?? null,
21
+ coreParameters: params.core ?? { type: 'type' },
22
+ settings: params.settings ?
23
+ new ParsedResourceSettings<T>(params.settings)
24
+ : new ParsedResourceSettings<T>({ id: 'type' }),
25
+ statefulMode: params.statefulMode ?? false,
26
+ })
27
+ }
28
+
29
+ export interface TestConfig extends StringIndexedObject {
30
+ propA: string;
31
+ propB: number;
32
+ propC?: string;
33
+ }
34
+
35
+ export class TestResource extends Resource<TestConfig> {
36
+ getSettings(): ResourceSettings<TestConfig> {
37
+ return { id: 'type' }
38
+ }
39
+
40
+ create(plan: CreatePlan<TestConfig>): Promise<void> {
41
+ return Promise.resolve(undefined);
42
+ }
43
+
44
+ destroy(plan: DestroyPlan<TestConfig>): Promise<void> {
45
+ return Promise.resolve(undefined);
46
+ }
47
+
48
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
49
+ return {
50
+ propA: 'a',
51
+ propB: 10,
52
+ propC: 'c',
53
+ };
54
+ }
55
+ }
56
+
57
+ export class TestStatefulParameter extends StatefulParameter<TestConfig, string> {
58
+ async refresh(desired: string | null): Promise<string | null> {
59
+ return 'd';
60
+ }
61
+
62
+ async add(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
63
+ return;
64
+ }
65
+
66
+ async modify(newValue: string, previousValue: string, plan: Plan<TestConfig>): Promise<void> {
67
+ return;
68
+ }
69
+
70
+ async remove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
71
+ return;
72
+ }
73
+ }
74
+
75
+ export class TestArrayStatefulParameter extends ArrayStatefulParameter<TestConfig, string> {
76
+ async refresh(): Promise<any | null> {
77
+ return ['3.11.9']
78
+ }
79
+
80
+ addItem(item: string, plan: Plan<TestConfig>): Promise<void> {
81
+ return Promise.resolve(undefined);
82
+ }
83
+
84
+ removeItem(item: string, plan: Plan<TestConfig>): Promise<void> {
85
+ return Promise.resolve(undefined);
86
+ }
87
+ }
@@ -3,7 +3,7 @@ import { splitUserConfig } from './utils.js';
3
3
 
4
4
  describe('Utils tests', () => {
5
5
  it('Can split a config correctly', () => {
6
- const { parameters, resourceMetadata } = splitUserConfig({
6
+ const { parameters, coreParameters } = splitUserConfig({
7
7
  type: 'type',
8
8
  name: 'name',
9
9
  dependsOn: ['a', 'b', 'c'],
@@ -13,7 +13,7 @@ describe('Utils tests', () => {
13
13
  propD: 'propD',
14
14
  })
15
15
 
16
- expect(resourceMetadata).toMatchObject({
16
+ expect(coreParameters).toMatchObject({
17
17
  type: 'type',
18
18
  name: 'name',
19
19
  dependsOn: ['a', 'b', 'c'],
@@ -1,6 +1,9 @@
1
1
  import promiseSpawn from '@npmcli/promise-spawn';
2
- import { SpawnOptions } from 'child_process';
3
2
  import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
+ import { SpawnOptions } from 'node:child_process';
4
+ import os from 'node:os';
5
+
6
+ import { ArrayParameterSetting } from '../resource/resource-settings.js';
4
7
 
5
8
  export enum SpawnStatus {
6
9
  SUCCESS = 'success',
@@ -81,21 +84,64 @@ export function isDebug(): boolean {
81
84
  }
82
85
 
83
86
  export function splitUserConfig<T extends StringIndexedObject>(
84
- config: T & ResourceConfig
85
- ): { parameters: T; resourceMetadata: ResourceConfig} {
86
- const resourceMetadata = {
87
+ config: ResourceConfig & T
88
+ ): { parameters: T; coreParameters: ResourceConfig } {
89
+ const coreParameters = {
87
90
  type: config.type,
88
91
  ...(config.name ? { name: config.name } : {}),
89
92
  ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
90
93
  };
91
94
 
95
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
92
96
  const { type, name, dependsOn, ...parameters } = config;
97
+
93
98
  return {
94
99
  parameters: parameters as T,
95
- resourceMetadata,
100
+ coreParameters,
96
101
  };
97
102
  }
98
103
 
99
104
  export function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean {
100
105
  return set1.size === set2.size && [...set1].every((v) => set2.has(v));
101
106
  }
107
+
108
+ const homeDirectory = os.homedir();
109
+
110
+ export function untildify(pathWithTilde: string) {
111
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
112
+ }
113
+
114
+ export function areArraysEqual(parameter: ArrayParameterSetting, desired: unknown, current: unknown) {
115
+ if (!Array.isArray(desired) || !Array.isArray(current)) {
116
+ throw new Error(`A non-array value:
117
+
118
+ Desired: ${JSON.stringify(desired, null, 2)}
119
+
120
+ Current: ${JSON.stringify(desired, null, 2)}
121
+
122
+ Was provided even though type array was specified.
123
+ `)
124
+ }
125
+
126
+ if (desired.length !== current.length) {
127
+ return false;
128
+ }
129
+
130
+ const desiredCopy = [...desired];
131
+ const currentCopy = [...current];
132
+
133
+ // Algorithm for to check equality between two un-ordered; un-hashable arrays using
134
+ // an isElementEqual method. Time: O(n^2)
135
+ for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
136
+ const idx = currentCopy.findIndex((e2) => (parameter.isElementEqual ?? ((a, b) => a === b))(desiredCopy[counter], e2))
137
+
138
+ if (idx === -1) {
139
+ return false;
140
+ }
141
+
142
+ desiredCopy.splice(counter, 1)
143
+ currentCopy.splice(idx, 1)
144
+ }
145
+
146
+ return currentCopy.length === 0;
147
+ }
package/tsconfig.json CHANGED
@@ -7,7 +7,6 @@
7
7
  "noImplicitAny": true,
8
8
  "resolveJsonModule": true,
9
9
  "esModuleInterop": true,
10
- "removeComments": true,
11
10
  "strictNullChecks": true,
12
11
  "emitDecoratorMetadata": true,
13
12
  "experimentalDecorators": true,
@@ -0,0 +1,10 @@
1
+ import { defaultExclude, defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ exclude: [
6
+ ...defaultExclude,
7
+ './src/utils/test-utils.test.ts'
8
+ ]
9
+ },
10
+ });