codify-plugin-lib 1.0.95 → 1.0.96

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.
@@ -81,46 +81,39 @@ export class ChangeSet {
81
81
  // Filter out null and undefined values or else the diff below will not work
82
82
  const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
83
83
  const current = Object.fromEntries(Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined));
84
- for (const [k, v] of Object.entries(current)) {
85
- if (desired?.[k] === null || desired?.[k] === undefined) {
84
+ for (const k of new Set([...Object.keys(current), ...Object.keys(desired)])) {
85
+ if (ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
86
86
  parameterChangeSet.push({
87
87
  name: k,
88
- previousValue: v ?? null,
88
+ previousValue: current[k] ?? null,
89
+ newValue: desired[k] ?? null,
90
+ operation: ParameterOperation.NOOP,
91
+ });
92
+ continue;
93
+ }
94
+ if ((desired?.[k] === null || desired?.[k] === undefined) && (current?.[k] !== null && current?.[k] !== undefined)) {
95
+ parameterChangeSet.push({
96
+ name: k,
97
+ previousValue: current[k] ?? null,
89
98
  newValue: null,
90
99
  operation: ParameterOperation.REMOVE,
91
100
  });
92
- delete current[k];
93
101
  continue;
94
102
  }
95
- if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
103
+ if ((current?.[k] === null || current?.[k] === undefined) && (desired?.[k] !== null && desired?.[k] !== undefined)) {
96
104
  parameterChangeSet.push({
97
105
  name: k,
98
- previousValue: v ?? null,
106
+ previousValue: null,
99
107
  newValue: desired[k] ?? null,
100
- operation: ParameterOperation.MODIFY,
108
+ operation: ParameterOperation.ADD,
101
109
  });
102
- delete current[k];
103
- delete desired[k];
104
110
  continue;
105
111
  }
106
112
  parameterChangeSet.push({
107
113
  name: k,
108
- previousValue: v ?? null,
114
+ previousValue: current[k] ?? null,
109
115
  newValue: desired[k] ?? null,
110
- operation: ParameterOperation.NOOP,
111
- });
112
- delete current[k];
113
- delete desired[k];
114
- }
115
- if (Object.keys(current).length > 0) {
116
- throw new Error('Diff algorithm error');
117
- }
118
- for (const [k, v] of Object.entries(desired)) {
119
- parameterChangeSet.push({
120
- name: k,
121
- previousValue: null,
122
- newValue: v ?? null,
123
- operation: ParameterOperation.ADD,
116
+ operation: ParameterOperation.MODIFY,
124
117
  });
125
118
  }
126
119
  return parameterChangeSet;
@@ -1,4 +1,4 @@
1
- import { resolveEqualsFn } from './resource-settings.js';
1
+ import { resolveEqualsFn, resolveParameterTransformFn } from './resource-settings.js';
2
2
  export class ParsedResourceSettings {
3
3
  cache = new Map();
4
4
  id;
@@ -61,8 +61,8 @@ export class ParsedResourceSettings {
61
61
  return {};
62
62
  }
63
63
  return Object.fromEntries(Object.entries(this.settings.parameterSettings)
64
- .filter(([, v]) => v.inputTransformation !== undefined)
65
- .map(([k, v]) => [k, v.inputTransformation]));
64
+ .filter(([, v]) => resolveParameterTransformFn(v) !== undefined)
65
+ .map(([k, v]) => [k, resolveParameterTransformFn(v)]));
66
66
  });
67
67
  }
68
68
  get statefulParameterOrder() {
@@ -112,7 +112,7 @@ export class ParsedResourceSettings {
112
112
  && schema.else.some((s) => s.required))) {
113
113
  throw new Error(`In the schema of ${this.settings.id}, a conditional required was declared (within anyOf, allOf, oneOf, else, or then) but an` +
114
114
  'import.requiredParameters was not found in the resource settings. This is required because Codify uses the required parameter to' +
115
- 'determine the prompt to ask users during imports. It can\'t parse which parameters are needed when' +
115
+ 'determine the prompt to ask users during imports. It can\'t parse which parameters are needed when ' +
116
116
  'required is declared conditionally.');
117
117
  }
118
118
  }
@@ -61,7 +61,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
61
61
  * config with desired config. Certain types will have additional options to help support it. For example the type
62
62
  * stateful requires a stateful parameter definition and type array takes an isElementEqual method.
63
63
  */
64
- export type ParameterSettingType = 'any' | 'array' | 'boolean' | 'directory' | 'number' | 'stateful' | 'string' | 'version';
64
+ export type ParameterSettingType = 'any' | 'array' | 'boolean' | 'directory' | 'number' | 'setting' | 'stateful' | 'string' | 'version';
65
65
  /**
66
66
  * Typing information for the parameter setting. This represents a setting on a specific parameter within a
67
67
  * resource. Options for configuring parameters operations including overriding the equals function, adding default values
@@ -150,3 +150,4 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
150
150
  order?: number;
151
151
  }
152
152
  export declare function resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean;
153
+ export declare function resolveParameterTransformFn(parameter: ParameterSetting): ((input: any) => Promise<any> | any) | undefined;
@@ -5,7 +5,8 @@ const ParameterEqualsDefaults = {
5
5
  'directory': (a, b) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
6
6
  'number': (a, b) => Number(a) === Number(b),
7
7
  'string': (a, b) => String(a) === String(b),
8
- 'version': (desired, current) => String(current).includes(String(desired))
8
+ 'version': (desired, current) => String(current).includes(String(desired)),
9
+ 'setting': (a, b) => true,
9
10
  };
10
11
  export function resolveEqualsFn(parameter, key) {
11
12
  if (parameter.type === 'array') {
@@ -16,3 +17,10 @@ export function resolveEqualsFn(parameter, key) {
16
17
  }
17
18
  return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
18
19
  }
20
+ const ParameterTransformationDefaults = {
21
+ 'directory': (a) => path.resolve(untildify(String(a))),
22
+ 'string': String,
23
+ };
24
+ export function resolveParameterTransformFn(parameter) {
25
+ return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type] ?? undefined;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.95",
3
+ "version": "1.0.96",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -141,53 +141,45 @@ export class ChangeSet<T extends StringIndexedObject> {
141
141
  Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined)
142
142
  ) as Partial<T>
143
143
 
144
- for (const [k, v] of Object.entries(current)) {
145
- if (desired?.[k] === null || desired?.[k] === undefined) {
144
+ for (const k of new Set([...Object.keys(current), ...Object.keys(desired)])) {
145
+ if (ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
146
146
  parameterChangeSet.push({
147
147
  name: k,
148
- previousValue: v ?? null,
148
+ previousValue: current[k] ?? null,
149
+ newValue: desired[k] ?? null,
150
+ operation: ParameterOperation.NOOP,
151
+ })
152
+
153
+ continue;
154
+ }
155
+
156
+ if ((desired?.[k] === null || desired?.[k] === undefined) && (current?.[k] !== null && current?.[k] !== undefined)) {
157
+ parameterChangeSet.push({
158
+ name: k,
159
+ previousValue: current[k] ?? null,
149
160
  newValue: null,
150
161
  operation: ParameterOperation.REMOVE,
151
162
  })
152
163
 
153
- delete current[k];
154
164
  continue;
155
165
  }
156
166
 
157
- if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
167
+ if ((current?.[k] === null || current?.[k] === undefined) && (desired?.[k] !== null && desired?.[k] !== undefined)) {
158
168
  parameterChangeSet.push({
159
169
  name: k,
160
- previousValue: v ?? null,
170
+ previousValue: null,
161
171
  newValue: desired[k] ?? null,
162
- operation: ParameterOperation.MODIFY,
172
+ operation: ParameterOperation.ADD,
163
173
  })
164
174
 
165
- delete current[k];
166
- delete desired[k];
167
175
  continue;
168
176
  }
169
177
 
170
178
  parameterChangeSet.push({
171
179
  name: k,
172
- previousValue: v ?? null,
180
+ previousValue: current[k] ?? null,
173
181
  newValue: desired[k] ?? null,
174
- operation: ParameterOperation.NOOP,
175
- })
176
-
177
- delete current[k];
178
- delete desired[k];
179
- }
180
-
181
- if (Object.keys(current).length > 0) {
182
- throw new Error('Diff algorithm error');
183
- }
184
-
185
- for (const [k, v] of Object.entries(desired)) {
186
- parameterChangeSet.push({
187
- name: k,
188
- previousValue: null,
189
- newValue: v ?? null,
190
- operation: ParameterOperation.ADD,
182
+ operation: ParameterOperation.MODIFY,
191
183
  })
192
184
  }
193
185
 
@@ -104,21 +104,21 @@ describe('Plugin tests', () => {
104
104
 
105
105
  it('Can get resource info', async () => {
106
106
  const schema = {
107
- "$schema": "http://json-schema.org/draft-07/schema",
108
- "$id": "https://www.codifycli.com/asdf-schema.json",
109
- "title": "Asdf resource",
110
- "type": "object",
111
- "properties": {
112
- "plugins": {
113
- "type": "array",
114
- "description": "Asdf plugins to install. See: https://github.com/asdf-community for a full list",
115
- "items": {
116
- "type": "string"
107
+ '$schema': 'http://json-schema.org/draft-07/schema',
108
+ '$id': 'https://www.codifycli.com/asdf-schema.json',
109
+ 'title': 'Asdf resource',
110
+ 'type': 'object',
111
+ 'properties': {
112
+ 'plugins': {
113
+ 'type': 'array',
114
+ 'description': 'Asdf plugins to install. See: https://github.com/asdf-community for a full list',
115
+ 'items': {
116
+ 'type': 'string'
117
117
  }
118
118
  }
119
119
  },
120
- "required": ["plugins"],
121
- "additionalProperties": false
120
+ 'required': ['plugins'],
121
+ 'additionalProperties': false
122
122
  }
123
123
 
124
124
 
@@ -142,21 +142,21 @@ describe('Plugin tests', () => {
142
142
 
143
143
  it('Get resource info to default import to the one specified in the resource settings', async () => {
144
144
  const schema = {
145
- "$schema": "http://json-schema.org/draft-07/schema",
146
- "$id": "https://www.codifycli.com/asdf-schema.json",
147
- "title": "Asdf resource",
148
- "type": "object",
149
- "properties": {
150
- "plugins": {
151
- "type": "array",
152
- "description": "Asdf plugins to install. See: https://github.com/asdf-community for a full list",
153
- "items": {
154
- "type": "string"
145
+ '$schema': 'http://json-schema.org/draft-07/schema',
146
+ '$id': 'https://www.codifycli.com/asdf-schema.json',
147
+ 'title': 'Asdf resource',
148
+ 'type': 'object',
149
+ 'properties': {
150
+ 'plugins': {
151
+ 'type': 'array',
152
+ 'description': 'Asdf plugins to install. See: https://github.com/asdf-community for a full list',
153
+ 'items': {
154
+ 'type': 'string'
155
155
  }
156
156
  }
157
157
  },
158
- "required": ["plugins"],
159
- "additionalProperties": false
158
+ 'required': ['plugins'],
159
+ 'additionalProperties': false
160
160
  }
161
161
 
162
162
 
@@ -62,8 +62,8 @@ export class Plugin {
62
62
  const schema = resource.settings.schema as JSONSchemaType<any> | undefined;
63
63
  const requiredPropertyNames = (
64
64
  resource.settings.import?.requiredParameters
65
- ?? schema?.required
66
- ?? null
65
+ ?? schema?.required
66
+ ?? null
67
67
  ) as null | string[];
68
68
 
69
69
  return {
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ResourceSettings } from './resource-settings.js';
3
3
  import { ParsedResourceSettings } from './parsed-resource-settings.js';
4
4
  import { TestConfig } from '../utils/test-utils.test.js';
5
- import { JSONSchemaType } from 'ajv';
6
5
 
7
6
  describe('Resource options parser tests', () => {
8
7
  it('Parses default values from options', () => {
@@ -25,37 +24,37 @@ describe('Resource options parser tests', () => {
25
24
 
26
25
  it('Throws an error when an import.requiredParameters is not declared', () => {
27
26
  const schema = {
28
- "$schema": "http://json-schema.org/draft-07/schema",
29
- "$id": "https://www.codifycli.com/git-clone.json",
30
- "title": "Git-clone resource",
31
- "type": "object",
32
- "properties": {
33
- "remote": {
34
- "type": "string",
35
- "description": "Remote tracking url to clone repo from. Equivalent to repository and only one should be specified"
27
+ '$schema': 'http://json-schema.org/draft-07/schema',
28
+ '$id': 'https://www.codifycli.com/git-clone.json',
29
+ 'title': 'Git-clone resource',
30
+ 'type': 'object',
31
+ 'properties': {
32
+ 'remote': {
33
+ 'type': 'string',
34
+ 'description': 'Remote tracking url to clone repo from. Equivalent to repository and only one should be specified'
36
35
  },
37
- "repository": {
38
- "type": "string",
39
- "description": "Remote repository to clone repo from. Equivalent to remote and only one should be specified"
36
+ 'repository': {
37
+ 'type': 'string',
38
+ 'description': 'Remote repository to clone repo from. Equivalent to remote and only one should be specified'
40
39
  },
41
- "parentDirectory": {
42
- "type": "string",
43
- "description": "Parent directory to clone into. The folder name will use default git semantics which extracts the last part of the clone url. Only one of parentDirectory or directory can be specified"
40
+ 'parentDirectory': {
41
+ 'type': 'string',
42
+ 'description': 'Parent directory to clone into. The folder name will use default git semantics which extracts the last part of the clone url. Only one of parentDirectory or directory can be specified'
44
43
  },
45
- "directory": {
46
- "type": "string",
47
- "description": "Directory to clone contents into. This value is directly passed into git clone. This differs from parent directory in that the last part of the path will be the folder name of the repo"
44
+ 'directory': {
45
+ 'type': 'string',
46
+ 'description': 'Directory to clone contents into. This value is directly passed into git clone. This differs from parent directory in that the last part of the path will be the folder name of the repo'
48
47
  },
49
- "autoVerifySSH": {
50
- "type": "boolean",
51
- "description": "Automatically verifies the ssh connection for ssh git clones. Defaults to true."
48
+ 'autoVerifySSH': {
49
+ 'type': 'boolean',
50
+ 'description': 'Automatically verifies the ssh connection for ssh git clones. Defaults to true.'
52
51
  }
53
52
  },
54
- "additionalProperties": false,
55
- "oneOf": [
56
- { "required": ["repository", "directory"] },
57
- { "required": ["repository", "parentDirectory"] },
58
- { "required": ["remote", "directory"] }
53
+ 'additionalProperties': false,
54
+ 'oneOf': [
55
+ { 'required': ['repository', 'directory'] },
56
+ { 'required': ['repository', 'parentDirectory'] },
57
+ { 'required': ['remote', 'directory'] }
59
58
  ]
60
59
  }
61
60
 
@@ -1,8 +1,14 @@
1
+ import { JSONSchemaType } from 'ajv';
1
2
  import { StringIndexedObject } from 'codify-schemas';
2
3
 
3
- import { ParameterSetting, resolveEqualsFn, ResourceSettings, StatefulParameterSetting } from './resource-settings.js';
4
+ import {
5
+ ParameterSetting,
6
+ ResourceSettings,
7
+ StatefulParameterSetting,
8
+ resolveEqualsFn,
9
+ resolveParameterTransformFn
10
+ } from './resource-settings.js';
4
11
  import { StatefulParameter as StatefulParameterImpl } from './stateful-parameter.js'
5
- import { JSONSchemaType } from 'ajv';
6
12
 
7
13
  export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
8
14
  private cache = new Map<string, unknown>();
@@ -88,8 +94,8 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
88
94
 
89
95
  return Object.fromEntries(
90
96
  Object.entries(this.settings.parameterSettings)
91
- .filter(([, v]) => v!.inputTransformation !== undefined)
92
- .map(([k, v]) => [k, v!.inputTransformation!] as const)
97
+ .filter(([, v]) => resolveParameterTransformFn(v!) !== undefined)
98
+ .map(([k, v]) => [k, resolveParameterTransformFn(v!)] as const)
93
99
  ) as Record<keyof T, (a: unknown) => unknown>;
94
100
  });
95
101
  }
@@ -157,7 +163,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
157
163
  ) {
158
164
  throw new Error(`In the schema of ${this.settings.id}, a conditional required was declared (within anyOf, allOf, oneOf, else, or then) but an` +
159
165
  'import.requiredParameters was not found in the resource settings. This is required because Codify uses the required parameter to' +
160
- 'determine the prompt to ask users during imports. It can\'t parse which parameters are needed when' +
166
+ 'determine the prompt to ask users during imports. It can\'t parse which parameters are needed when ' +
161
167
  'required is declared conditionally.'
162
168
  )
163
169
  }
@@ -11,6 +11,8 @@ import {
11
11
  } from '../utils/test-utils.test.js';
12
12
  import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
13
13
  import { ResourceController } from './resource-controller.js';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
14
16
 
15
17
  describe('Resource parameter tests', () => {
16
18
  it('Generates a resource plan that includes stateful parameters (create)', async () => {
@@ -540,4 +542,74 @@ describe('Resource parameter tests', () => {
540
542
  }
541
543
  };
542
544
  })
545
+
546
+ it('Applies default input transformations', async () => {
547
+ const home = os.homedir()
548
+ const testPath = path.join(home, 'test/folder');
549
+
550
+ const resource = new class extends TestResource {
551
+ getSettings(): ResourceSettings<TestConfig> {
552
+ return {
553
+ id: 'resourceType',
554
+ parameterSettings: {
555
+ propA: { type: 'directory' }
556
+ }
557
+ }
558
+ }
559
+
560
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
561
+ return { propA: testPath }
562
+ }
563
+ };
564
+
565
+ const controller = new ResourceController(resource);
566
+ const plan = await controller.plan({ type: 'resourceType', propA: '~/test/folder' } as any);
567
+
568
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
569
+ operation: ParameterOperation.NOOP,
570
+ newValue: testPath,
571
+ previousValue: testPath,
572
+ })
573
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
574
+ })
575
+
576
+ it('Ignores setting parameters when planning', async () => {
577
+ const resource = new class extends TestResource {
578
+ getSettings(): ResourceSettings<TestConfig> {
579
+ return {
580
+ id: 'resourceType',
581
+ parameterSettings: {
582
+ propA: { type: 'setting' },
583
+ propB: { type: 'number' }
584
+ }
585
+ }
586
+ }
587
+
588
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
589
+ return { propB: 64 }
590
+ }
591
+ };
592
+
593
+ const controller = new ResourceController(resource);
594
+ const plan = await controller.plan({ type: 'resourceType', propA: 'setting', propB: 64 } as any);
595
+
596
+ expect(plan.changeSet.parameterChanges).toMatchObject(
597
+ expect.arrayContaining([
598
+ {
599
+ name: 'propA',
600
+ operation: ParameterOperation.NOOP,
601
+ previousValue: null,
602
+ newValue: 'setting',
603
+ },
604
+ {
605
+ name: 'propB',
606
+ operation: ParameterOperation.NOOP,
607
+ previousValue: 64,
608
+ newValue: 64,
609
+ }
610
+ ])
611
+ )
612
+
613
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
614
+ })
543
615
  })
@@ -81,6 +81,7 @@ export type ParameterSettingType =
81
81
  | 'boolean'
82
82
  | 'directory'
83
83
  | 'number'
84
+ | 'setting'
84
85
  | 'stateful'
85
86
  | 'string'
86
87
  | 'version';
@@ -192,10 +193,10 @@ const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown,
192
193
  'directory': (a: unknown, b: unknown) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))),
193
194
  'number': (a: unknown, b: unknown) => Number(a) === Number(b),
194
195
  'string': (a: unknown, b: unknown) => String(a) === String(b),
195
- 'version': (desired: unknown, current: unknown) => String(current).includes(String(desired))
196
+ 'version': (desired: unknown, current: unknown) => String(current).includes(String(desired)),
197
+ 'setting': (a: unknown, b: unknown) => true,
196
198
  }
197
199
 
198
-
199
200
  export function resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean {
200
201
  if (parameter.type === 'array') {
201
202
  return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
@@ -207,3 +208,14 @@ export function resolveEqualsFn(parameter: ParameterSetting, key: string): (desi
207
208
 
208
209
  return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
209
210
  }
211
+
212
+ const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, (input: any) => Promise<any> | any>> = {
213
+ 'directory': (a: unknown) => path.resolve(untildify(String(a))),
214
+ 'string': String,
215
+ }
216
+
217
+ export function resolveParameterTransformFn(
218
+ parameter: ParameterSetting
219
+ ): ((input: any) => Promise<any> | any) | undefined {
220
+ return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined;
221
+ }