Skip to content

Commit 365874c

Browse files
feat: add import command (#11)
* Add getResourceInfo response and request * Add import functionality * Add import customization block * Improved diff algorithm in change-set, added setting parameter type and added transformation defaults for string and directory * Added array filtering for all array parameters and made it customizable with a parameter. Added documentation for the new import parameters
1 parent 8a0c466 commit 365874c

14 files changed

+722
-61
lines changed

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codify-plugin-lib",
3-
"version": "1.0.88",
3+
"version": "1.0.100",
44
"description": "Library plugin library",
55
"main": "dist/index.js",
66
"typings": "dist/index.d.ts",
@@ -14,7 +14,7 @@
1414
"dependencies": {
1515
"ajv": "^8.12.0",
1616
"ajv-formats": "^2.1.1",
17-
"codify-schemas": "1.0.45",
17+
"codify-schemas": "1.0.52",
1818
"@npmcli/promise-spawn": "^7.0.1",
1919
"uuid": "^10.0.0"
2020
},

src/messages/handlers.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import addFormats from 'ajv-formats';
33
import {
44
ApplyRequestDataSchema,
55
ApplyResponseDataSchema,
6+
GetResourceInfoRequestDataSchema,
7+
GetResourceInfoResponseDataSchema,
8+
ImportRequestDataSchema,
9+
ImportResponseDataSchema,
610
InitializeRequestDataSchema,
711
InitializeResponseDataSchema,
812
IpcMessage,
@@ -19,29 +23,39 @@ import { SudoError } from '../errors.js';
1923
import { Plugin } from '../plugin/plugin.js';
2024

2125
const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) => Promise<unknown>; requestValidator: SchemaObject; responseValidator: SchemaObject }> = {
22-
'apply': {
23-
async handler(plugin: Plugin, data: any) {
24-
await plugin.apply(data);
25-
return null;
26-
},
27-
requestValidator: ApplyRequestDataSchema,
28-
responseValidator: ApplyResponseDataSchema
29-
},
3026
'initialize': {
3127
handler: async (plugin: Plugin) => plugin.initialize(),
3228
requestValidator: InitializeRequestDataSchema,
3329
responseValidator: InitializeResponseDataSchema
3430
},
31+
'validate': {
32+
handler: async (plugin: Plugin, data: any) => plugin.validate(data),
33+
requestValidator: ValidateRequestDataSchema,
34+
responseValidator: ValidateResponseDataSchema
35+
},
36+
'getResourceInfo': {
37+
handler: async (plugin: Plugin, data: any) => plugin.getResourceInfo(data),
38+
requestValidator: GetResourceInfoRequestDataSchema,
39+
responseValidator: GetResourceInfoResponseDataSchema
40+
},
41+
'import': {
42+
handler: async (plugin: Plugin, data: any) => plugin.import(data),
43+
requestValidator: ImportRequestDataSchema,
44+
responseValidator: ImportResponseDataSchema
45+
},
3546
'plan': {
3647
handler: async (plugin: Plugin, data: any) => plugin.plan(data),
3748
requestValidator: PlanRequestDataSchema,
3849
responseValidator: PlanResponseDataSchema
3950
},
40-
'validate': {
41-
handler: async (plugin: Plugin, data: any) => plugin.validate(data),
42-
requestValidator: ValidateRequestDataSchema,
43-
responseValidator: ValidateResponseDataSchema
44-
}
51+
'apply': {
52+
async handler(plugin: Plugin, data: any) {
53+
await plugin.apply(data);
54+
return null;
55+
},
56+
requestValidator: ApplyRequestDataSchema,
57+
responseValidator: ApplyResponseDataSchema
58+
},
4559
}
4660

4761
export class MessageHandler {

src/plan/change-set.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -141,53 +141,45 @@ export class ChangeSet<T extends StringIndexedObject> {
141141
Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined)
142142
) as Partial<T>
143143

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])) {
146146
parameterChangeSet.push({
147147
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,
149160
newValue: null,
150161
operation: ParameterOperation.REMOVE,
151162
})
152163

153-
delete current[k];
154164
continue;
155165
}
156166

157-
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
167+
if ((current?.[k] === null || current?.[k] === undefined) && (desired?.[k] !== null && desired?.[k] !== undefined)) {
158168
parameterChangeSet.push({
159169
name: k,
160-
previousValue: v ?? null,
170+
previousValue: null,
161171
newValue: desired[k] ?? null,
162-
operation: ParameterOperation.MODIFY,
172+
operation: ParameterOperation.ADD,
163173
})
164174

165-
delete current[k];
166-
delete desired[k];
167175
continue;
168176
}
169177

170178
parameterChangeSet.push({
171179
name: k,
172-
previousValue: v ?? null,
180+
previousValue: current[k] ?? null,
173181
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,
191183
})
192184
}
193185

src/plan/plan.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,84 @@ describe('Plan entity tests', () => {
147147
operation: ResourceOperation.RECREATE
148148
})
149149
})
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: {
187+
type: 'array',
188+
canModify: true,
189+
isElementEqual: (a, b) => b.includes(a),
190+
filterInStatelessMode: false
191+
}
192+
}
193+
}
194+
}
195+
196+
async refresh(): Promise<Partial<any> | null> {
197+
return {
198+
propZ: [
199+
'20.15.0',
200+
'20.15.1'
201+
]
202+
}
203+
}
204+
}
205+
206+
const controller = new ResourceController(resource);
207+
const plan = await controller.plan({
208+
propZ: ['20.15'],
209+
} as any)
210+
211+
expect(plan.changeSet).toMatchObject({
212+
operation: ResourceOperation.MODIFY,
213+
parameterChanges: expect.arrayContaining([
214+
expect.objectContaining({
215+
name: 'propZ',
216+
previousValue: expect.arrayContaining([
217+
'20.15.0',
218+
'20.15.1'
219+
]),
220+
newValue: expect.arrayContaining([
221+
'20.15'
222+
]),
223+
operation: 'modify'
224+
})
225+
])
226+
})
227+
})
150228
})
151229

152230
function createTestResource() {

src/plan/plan.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export class Plan<T extends StringIndexedObject> {
220220
// TODO: Add object handling here in addition to arrays in the future
221221
const arrayStatefulParameters = Object.fromEntries(
222222
Object.entries(filteredCurrent)
223-
.filter(([k, v]) => isArrayStatefulParameter(k, v))
223+
.filter(([k, v]) => isArrayParameterWithFiltering(k, v))
224224
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
225225
)
226226

@@ -247,19 +247,27 @@ export class Plan<T extends StringIndexedObject> {
247247
) as Partial<T>;
248248
}
249249

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+
))
253258
&& Array.isArray(v)
254259
}
255260

256261
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
257262
function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
258263
const desiredArray = desired![k] as unknown[];
259-
const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
264+
const matcher = settings.parameterSettings![k]!.type === 'stateful'
265+
? ((settings.parameterSettings![k] as StatefulParameterSetting)
260266
.definition
261267
.getSettings() as ArrayParameterSetting)
262-
.isElementEqual ?? ((a, b) => a === b);
268+
.isElementEqual ?? ((a, b) => a === b)
269+
: (settings.parameterSettings![k] as ArrayParameterSetting)
270+
.isElementEqual ?? ((a, b) => a === b)
263271

264272
const desiredCopy = [...desiredArray];
265273
const currentCopy = [...v];

src/plugin/plugin.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,81 @@ describe('Plugin tests', () => {
101101
await testPlugin.apply({ plan })
102102
expect(resource.modify.calledOnce).to.be.true;
103103
});
104+
105+
it('Can get resource info', async () => {
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'
117+
}
118+
}
119+
},
120+
'required': ['plugins'],
121+
'additionalProperties': false
122+
}
123+
124+
125+
const resource = new class extends TestResource {
126+
getSettings(): ResourceSettings<TestConfig> {
127+
return {
128+
id: 'typeId',
129+
schema,
130+
}
131+
}
132+
}
133+
const testPlugin = Plugin.create('testPlugin', [resource as any])
134+
135+
const resourceInfo = await testPlugin.getResourceInfo({ type: 'typeId' })
136+
expect(resourceInfo.import).toMatchObject({
137+
requiredParameters: [
138+
'plugins'
139+
]
140+
})
141+
})
142+
143+
it('Get resource info to default import to the one specified in the resource settings', async () => {
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'
155+
}
156+
}
157+
},
158+
'required': ['plugins'],
159+
'additionalProperties': false
160+
}
161+
162+
163+
const resource = new class extends TestResource {
164+
getSettings(): ResourceSettings<TestConfig> {
165+
return {
166+
id: 'typeId',
167+
schema,
168+
import: {
169+
requiredParameters: []
170+
}
171+
}
172+
}
173+
}
174+
const testPlugin = Plugin.create('testPlugin', [resource as any])
175+
176+
const resourceInfo = await testPlugin.getResourceInfo({ type: 'typeId' })
177+
expect(resourceInfo.import).toMatchObject({
178+
requiredParameters: []
179+
})
180+
})
104181
});

0 commit comments

Comments
 (0)