diff --git a/.changeset/tall-ducks-join.md b/.changeset/tall-ducks-join.md new file mode 100644 index 000000000..1ed438b14 --- /dev/null +++ b/.changeset/tall-ducks-join.md @@ -0,0 +1,5 @@ +--- +'@roadiehq/scaffolder-backend-module-utils': minor +--- + +Add merge array value option diff --git a/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.test.ts b/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.test.ts index ba08a7177..ab47c9bf1 100644 --- a/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.test.ts +++ b/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.test.ts @@ -88,7 +88,8 @@ describe('roadiehq:utils:json:merge', () => { it('should append the content to the file if it already exists', async () => { mock({ 'fake-tmp-dir': { - 'fake-file.json': '{ "scripts": { "lsltr": "ls -ltr" } }', + 'fake-file.json': + '{ "scripts": { "lsltr": "ls -ltr" }, "array": ["first item"] }', }, }); @@ -101,6 +102,7 @@ describe('roadiehq:utils:json:merge', () => { scripts: { lsltrh: 'ls -ltrh', }, + array: ['second item'], }, }, }); @@ -109,6 +111,41 @@ describe('roadiehq:utils:json:merge', () => { const file = fs.readFileSync('fake-tmp-dir/fake-file.json', 'utf-8'); expect(JSON.parse(file)).toEqual({ scripts: { lsltr: 'ls -ltr', lsltrh: 'ls -ltrh' }, + array: ['second item'], + }); + }); + + it('should merge arrays if configured', async () => { + mock({ + 'fake-tmp-dir': { + 'fake-file.json': + '{ "scripts": { "lsltr": "ls -ltr" }, "array": ["first item"] }', + }, + }); + + await action.handler({ + ...mockContext, + workspacePath: 'fake-tmp-dir', + input: { + path: 'fake-file.json', + mergeArrays: true, + content: { + scripts: { + lsltrh: 'ls -ltrh', + }, + array: ['second item'], + }, + }, + }); + + expect(fs.existsSync('fake-tmp-dir/fake-file.json')).toBe(true); + const file = fs.readFileSync('fake-tmp-dir/fake-file.json', 'utf-8'); + expect(JSON.parse(file)).toEqual({ + scripts: { + lsltr: 'ls -ltr', + lsltrh: 'ls -ltrh', + }, + array: ['first item', 'second item'], }); }); @@ -277,7 +314,7 @@ scripts: it('can merge content into a yaml file', async () => { mock({ 'fake-tmp-dir': { - 'fake-file.yaml': 'scripts:\n lsltr: ls -ltr\n', + 'fake-file.yaml': 'array: ["first item"]\nscripts:\n lsltr: ls -ltr\n', }, }); @@ -290,6 +327,7 @@ scripts: scripts: { lsltrh: 'ls -ltrh', }, + array: ['second item'], }, }, }); @@ -298,6 +336,33 @@ scripts: const file = fs.readFileSync('fake-tmp-dir/fake-file.yaml', 'utf-8'); expect(yaml.load(file)).toEqual({ scripts: { lsltr: 'ls -ltr', lsltrh: 'ls -ltrh' }, + array: ['second item'], + }); + }); + + it('can merge arrays if configured', async () => { + mock({ + 'fake-tmp-dir': { + 'fake-file.yaml': "array: ['first item']", + }, + }); + + await action.handler({ + ...mockContext, + workspacePath: 'fake-tmp-dir', + input: { + path: 'fake-file.yaml', + mergeArrays: true, + content: { + array: ['second item'], + }, + }, + }); + + expect(fs.existsSync('fake-tmp-dir/fake-file.yaml')).toBe(true); + const file = fs.readFileSync('fake-tmp-dir/fake-file.yaml', 'utf-8'); + expect(yaml.load(file)).toEqual({ + array: ['first item', 'second item'], }); }); diff --git a/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.ts b/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.ts index b6616771a..b005b9cc7 100644 --- a/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.ts +++ b/plugins/scaffolder-actions/scaffolder-backend-module-utils/src/actions/merge/merge.ts @@ -18,12 +18,23 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolveSafeChildPath } from '@backstage/backend-common'; import fs from 'fs-extra'; import { extname } from 'path'; -import { merge } from 'lodash'; +import { isArray, isNull, mergeWith } from 'lodash'; import yaml from 'js-yaml'; import { supportedDumpOptions, yamlOptionsSchema } from '../../types'; +function mergeArrayCustomiser(objValue: string | any[], srcValue: any) { + if (isArray(objValue) && !isNull(objValue)) { + return Array.from(new Set(objValue.concat(srcValue))); + } + return undefined; +} + export function createMergeJSONAction({ actionId }: { actionId?: string }) { - return createTemplateAction<{ path: string; content: any }>({ + return createTemplateAction<{ + path: string; + content: any; + mergeArrays?: boolean; + }>({ id: actionId || 'roadiehq:utils:json:merge', description: 'Merge new data into an existing JSON file.', supportsDryRun: true, @@ -43,6 +54,13 @@ export function createMergeJSONAction({ actionId }: { actionId?: string }) { title: 'Content', type: ['string', 'object'], }, + mergeArrays: { + type: 'boolean', + default: false, + title: 'Merge Arrays?', + description: + 'Where a value is an array the merge function should concatenate the provided array value with the target array', + }, }, }, output: { @@ -80,7 +98,15 @@ export function createMergeJSONAction({ actionId }: { actionId?: string }) { fs.writeFileSync( sourceFilepath, - JSON.stringify(merge(existingContent, content), null, 2), + JSON.stringify( + mergeWith( + existingContent, + content, + ctx.input.mergeArrays ? mergeArrayCustomiser : undefined, + ), + null, + 2, + ), ); ctx.output('path', sourceFilepath); }, @@ -91,6 +117,7 @@ export function createMergeAction() { return createTemplateAction<{ path: string; content: any; + mergeArrays?: boolean; options?: supportedDumpOptions; }>({ id: 'roadiehq:utils:merge', @@ -112,6 +139,13 @@ export function createMergeAction() { title: 'Content', type: ['string', 'object'], }, + mergeArrays: { + type: 'boolean', + default: false, + title: 'Merge Arrays?', + description: + 'Where a value is an array the merge function should concatenate the provided array value with the target array', + }, options: { ...yamlOptionsSchema, description: `${yamlOptionsSchema.description} (for YAML output only)`, @@ -148,7 +182,11 @@ export function createMergeAction() { ? JSON.parse(ctx.input.content) : ctx.input.content; // This supports the case where dynamic keys are required mergedContent = JSON.stringify( - merge(JSON.parse(originalContent), newContent), + mergeWith( + yaml.load(originalContent), + newContent, + ctx.input.mergeArrays ? mergeArrayCustomiser : undefined, + ), null, 2, ); @@ -161,7 +199,11 @@ export function createMergeAction() { ? yaml.load(ctx.input.content) : ctx.input.content; // This supports the case where dynamic keys are required mergedContent = yaml.dump( - merge(yaml.load(originalContent), newContent), + mergeWith( + yaml.load(originalContent), + newContent, + ctx.input.mergeArrays ? mergeArrayCustomiser : undefined, + ), ctx.input.options, ); break;