From 7d1804d16ebff87ee78a5c88a210eac7d26b59df Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 6 Dec 2024 09:39:34 +0100 Subject: [PATCH] feat: prune `layoutAssignments`, `loginHours` and `loginIpRanges` (#954) --- __tests__/unit/lib/utils/fxpHelper.test.ts | 27 -- __tests__/unit/lib/utils/metadataDiff.test.ts | 253 ++++++++++- src/metadata/a48.json | 28 +- src/metadata/v46.json | 25 ++ src/metadata/v47.json | 25 ++ src/metadata/v49.json | 25 ++ src/metadata/v50.json | 25 ++ src/metadata/v51.json | 25 ++ src/metadata/v52.json | 25 ++ src/metadata/v53.json | 25 ++ src/metadata/v54.json | 25 ++ src/metadata/v55.json | 25 ++ src/metadata/v56.json | 25 ++ src/metadata/v57.json | 25 ++ src/metadata/v58.json | 25 ++ src/metadata/v59.json | 25 ++ src/metadata/v60.json | 25 ++ src/metadata/v61.json | 25 ++ src/metadata/v62.json | 25 ++ .../flowTranslationProcessor.ts | 7 +- src/service/inFileHandler.ts | 2 +- src/service/objectTranslationHandler.ts | 6 +- src/utils/fxpHelper.ts | 4 - src/utils/metadataDiff.ts | 395 +++++++++--------- 24 files changed, 876 insertions(+), 246 deletions(-) diff --git a/__tests__/unit/lib/utils/fxpHelper.test.ts b/__tests__/unit/lib/utils/fxpHelper.test.ts index da82f9f74..d47bdf6d7 100644 --- a/__tests__/unit/lib/utils/fxpHelper.test.ts +++ b/__tests__/unit/lib/utils/fxpHelper.test.ts @@ -4,7 +4,6 @@ import { describe, expect, it, jest } from '@jest/globals' import type { Config } from '../../../../src/types/config' import { readPathFromGit } from '../../../../src/utils/fsHelper' import { - asArray, convertJsonToXml, parseXmlFileToJson, xml2Json, @@ -15,32 +14,6 @@ const mockedReadPathFromGit = jest.mocked(readPathFromGit) jest.mock('../../../../src/utils/fsHelper') describe('fxpHelper', () => { - describe('asArray', () => { - describe('when called with array', () => { - // Arrange - const expected = ['test'] - - it('returns the same array', () => { - // Act - const actual = asArray(expected) - - // Assert - expect(actual).toBe(expected) - }) - }) - describe('when called with object', () => { - // Arrange - const expected = 'test' - - it('returns the array with this object', () => { - // Act - const actual = asArray(expected) - - // Assert - expect(actual).toEqual([expected]) - }) - }) - }) describe('parseXmlFileToJson', () => { const config: Config = { from: '', diff --git a/__tests__/unit/lib/utils/metadataDiff.test.ts b/__tests__/unit/lib/utils/metadataDiff.test.ts index 5bcc73574..46c168ffa 100644 --- a/__tests__/unit/lib/utils/metadataDiff.test.ts +++ b/__tests__/unit/lib/utils/metadataDiff.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it, jest } from '@jest/globals' import { MetadataRepository } from '../../../../src/metadata/MetadataRepository' +import { getInFileAttributes } from '../../../../src/metadata/metadataManager' +import { SharedFileMetadata } from '../../../../src/types/metadata' import type { Work } from '../../../../src/types/work' import { convertJsonToXml, @@ -22,12 +24,105 @@ jest.mock('../../../../src/utils/fxpHelper', () => { }) const mockedParseXmlFileToJson = jest.mocked(parseXmlFileToJson) -const workFlowAttributes = new Map([ - ['alerts', { xmlName: 'WorkflowAlert', key: 'fullName' }], -]) - const xmlHeader = { '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' } } +const emptyProfile = { + Profile: { + '@_xmlns': 'http://soap.sforce.com/2006/04/metadata', + layoutAssignments: [], + loginHours: [], + loginIpRanges: [], + }, +} + +const profile = { + Profile: { + '@_xmlns': 'http://soap.sforce.com/2006/04/metadata', + layoutAssignments: [ + { + layout: 'test-layout', + recordType: 'test-recordType', + }, + ], + loginHours: [ + { + mondayStart: '300', + mondayEnd: '500', + }, + ], + loginIpRanges: [ + { + description: 'ip range description', + endAddress: '168.0.0.1', + startAddress: '168.0.0.255', + }, + ], + }, +} + +const profileChanged = { + Profile: { + '@_xmlns': 'http://soap.sforce.com/2006/04/metadata', + layoutAssignments: [ + { + layout: 'another-test-layout', + recordType: 'test-recordType', + }, + ], + loginHours: [ + { + mondayStart: '400', + mondayEnd: '500', + }, + ], + loginIpRanges: [ + { + description: 'ip range description', + endAddress: '168.0.0.0', + startAddress: '168.0.0.255', + }, + ], + }, +} + +const profileAdded = { + Profile: { + '@_xmlns': 'http://soap.sforce.com/2006/04/metadata', + layoutAssignments: [ + { + layout: 'test-layout', + recordType: 'test-recordType', + }, + { + layout: 'another-test-layout', + recordType: 'test-recordType', + }, + ], + loginHours: [ + { + mondayStart: '300', + mondayEnd: '500', + }, + { + tuesdayStart: '400', + tuesdayEnd: '500', + }, + ], + loginIpRanges: [ + { + description: 'ip range description', + endAddress: '168.0.0.0', + startAddress: '168.0.0.255', + }, + { + description: 'complete ip range description', + endAddress: '168.0.0.1', + startAddress: '168.0.0.255', + }, + ], + }, +} + const alert = { Workflow: { '@_xmlns': 'http://soap.sforce.com/2006/04/metadata', @@ -104,20 +199,18 @@ const unTracked = { describe.each([[{}], [xmlHeader]])(`MetadataDiff`, header => { let metadataDiff: MetadataDiff let globalMetadata: MetadataRepository + let inFileAttribute: Map let work: Work beforeAll(async () => { globalMetadata = await getGlobalMetadata() + inFileAttribute = getInFileAttributes(globalMetadata) }) beforeEach(() => { jest.resetAllMocks() work = getWork() work.config.from = 'from' work.config.to = 'to' - metadataDiff = new MetadataDiff( - work.config, - globalMetadata, - workFlowAttributes - ) + metadataDiff = new MetadataDiff(work.config, inFileAttribute) }) describe(`compare with ${JSON.stringify(header)} header`, () => { @@ -291,6 +384,148 @@ describe.each([[{}], [xmlHeader]])(`MetadataDiff`, header => { expect(isEmpty).toBe(false) }) + describe('key less elements', () => { + it('given one element modified, the generated file contains the difference', async () => { + /* + Cas loginHours et loginIpRanges = si les tableaux sont égaux => on met tableau vide sinon on met le dernier tableau + Cas layout : ajouter tous les éléments de to qui ne sont pas dans from + */ + // Arrange + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profileChanged, + }) + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profile, + }) + await metadataDiff.compare('file/path') + + // Act + const { isEmpty } = metadataDiff.prune() + + // Assert + expect(convertJsonToXml).toHaveBeenCalledWith({ + ...header, + ...profileChanged, + }) + expect(isEmpty).toBe(false) + }) + + it('given added elements, the generated file contains the difference', async () => { + /* + Cas loginHours et loginIpRanges = si les tableaux sont égaux => on met tableau vide sinon on met le dernier tableau + Cas layout : ajouter tous les éléments de to qui ne sont pas dans from + */ + // Arrange + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profileAdded, + }) + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profile, + }) + await metadataDiff.compare('file/path') + + // Act + const { isEmpty } = metadataDiff.prune() + + // Assert + expect(convertJsonToXml).toHaveBeenCalledWith({ + ...header, + ...{ + Profile: { + '@_xmlns': 'http://soap.sforce.com/2006/04/metadata', + layoutAssignments: [ + { + layout: 'another-test-layout', + recordType: 'test-recordType', + }, + ], + loginHours: [ + { + mondayStart: '300', + mondayEnd: '500', + }, + { + tuesdayStart: '400', + tuesdayEnd: '500', + }, + ], + loginIpRanges: [ + { + description: 'ip range description', + endAddress: '168.0.0.0', + startAddress: '168.0.0.255', + }, + { + description: 'complete ip range description', + endAddress: '168.0.0.1', + startAddress: '168.0.0.255', + }, + ], + }, + }, + }) + expect(isEmpty).toBe(false) + }) + + it('given no element added nor modified, the generated file contains empty definition', async () => { + /* + Cas loginHours et loginIpRanges = si les tableaux sont égaux => on met tableau vide sinon on met le dernier tableau + Cas layout : ajouter tous les éléments de to qui ne sont pas dans from + */ + // Arrange + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profile, + }) + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profile, + }) + await metadataDiff.compare('file/path') + + // Act + const { isEmpty } = metadataDiff.prune() + + // Assert + expect(convertJsonToXml).toHaveBeenCalledWith({ + ...header, + ...emptyProfile, + }) + expect(isEmpty).toBe(true) + }) + + it('given no element added nor modified, the generated file contains empty profile', async () => { + /* + Cas loginHours et loginIpRanges = si les tableaux sont égaux => on met tableau vide sinon on met le dernier tableau + Cas layout : ajouter tous les éléments de to qui ne sont pas dans from + */ + // Arrange + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...emptyProfile, + }) + mockedParseXmlFileToJson.mockResolvedValueOnce({ + ...header, + ...profile, + }) + await metadataDiff.compare('file/path') + + // Act + const { isEmpty } = metadataDiff.prune() + + // Assert + expect(convertJsonToXml).toHaveBeenCalledWith({ + ...header, + ...emptyProfile, + }) + expect(isEmpty).toBe(true) + }) + }) + it('given untracked element, nothing trackable changed, the generated file contains untracked elements', async () => { // Arrange mockedParseXmlFileToJson.mockResolvedValueOnce({ diff --git a/src/metadata/a48.json b/src/metadata/a48.json index 5b8d001b4..4786d7ac9 100644 --- a/src/metadata/a48.json +++ b/src/metadata/a48.json @@ -1505,8 +1505,7 @@ "xmlTag": "fieldPermissions", "key": "field", "excluded": true - }, - { + },{ "inFolder": false, "metaFile": false, "parentXmlName": "Profile", @@ -1515,6 +1514,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v46.json b/src/metadata/v46.json index 79f7043fd..3b35e17a6 100644 --- a/src/metadata/v46.json +++ b/src/metadata/v46.json @@ -1283,6 +1283,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v47.json b/src/metadata/v47.json index 5c3920046..8817d8e43 100644 --- a/src/metadata/v47.json +++ b/src/metadata/v47.json @@ -1515,6 +1515,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v49.json b/src/metadata/v49.json index 3228a8e18..a62eaae61 100644 --- a/src/metadata/v49.json +++ b/src/metadata/v49.json @@ -1543,6 +1543,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v50.json b/src/metadata/v50.json index 813decf2f..c5f6137d1 100644 --- a/src/metadata/v50.json +++ b/src/metadata/v50.json @@ -1578,6 +1578,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v51.json b/src/metadata/v51.json index 2270d3784..5e4291ba8 100644 --- a/src/metadata/v51.json +++ b/src/metadata/v51.json @@ -1648,6 +1648,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v52.json b/src/metadata/v52.json index 21b75dd19..23ec24871 100644 --- a/src/metadata/v52.json +++ b/src/metadata/v52.json @@ -1655,6 +1655,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v53.json b/src/metadata/v53.json index 21b75dd19..23ec24871 100644 --- a/src/metadata/v53.json +++ b/src/metadata/v53.json @@ -1655,6 +1655,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v54.json b/src/metadata/v54.json index 3c8ea566f..467488c91 100644 --- a/src/metadata/v54.json +++ b/src/metadata/v54.json @@ -1708,6 +1708,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v55.json b/src/metadata/v55.json index 5d10addc2..81971871d 100644 --- a/src/metadata/v55.json +++ b/src/metadata/v55.json @@ -1806,6 +1806,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v56.json b/src/metadata/v56.json index a94f767a7..84f97b4cf 100644 --- a/src/metadata/v56.json +++ b/src/metadata/v56.json @@ -1834,6 +1834,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v57.json b/src/metadata/v57.json index 1d5e90022..88eb1567d 100644 --- a/src/metadata/v57.json +++ b/src/metadata/v57.json @@ -1883,6 +1883,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v58.json b/src/metadata/v58.json index 7960a37a7..73a053538 100644 --- a/src/metadata/v58.json +++ b/src/metadata/v58.json @@ -1890,6 +1890,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v59.json b/src/metadata/v59.json index 1a9fb1434..5f549b4a0 100644 --- a/src/metadata/v59.json +++ b/src/metadata/v59.json @@ -1912,6 +1912,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v60.json b/src/metadata/v60.json index 71e1639c2..13c896bfb 100644 --- a/src/metadata/v60.json +++ b/src/metadata/v60.json @@ -1946,6 +1946,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v61.json b/src/metadata/v61.json index 31bc0c62b..8cb06d909 100644 --- a/src/metadata/v61.json +++ b/src/metadata/v61.json @@ -1953,6 +1953,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v62.json b/src/metadata/v62.json index 31bc0c62b..8cb06d909 100644 --- a/src/metadata/v62.json +++ b/src/metadata/v62.json @@ -1953,6 +1953,31 @@ "key": "flow", "excluded": true }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLayoutAssignments", + "xmlTag": "layoutAssignments", + "key": "layout", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginHours", + "xmlTag": "loginHours", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/post-processor/flowTranslationProcessor.ts b/src/post-processor/flowTranslationProcessor.ts index 764a7eac8..4b92c4679 100644 --- a/src/post-processor/flowTranslationProcessor.ts +++ b/src/post-processor/flowTranslationProcessor.ts @@ -1,4 +1,6 @@ 'use strict' + +import { castArray } from 'lodash' import { join, parse } from 'path/posix' import { pathExists } from 'fs-extra' @@ -14,7 +16,6 @@ import type { Work } from '../types/work' import { readDir, writeFile } from '../utils/fsHelper' import { isSamePath, isSubDir, readFile } from '../utils/fsUtils' import { - asArray, convertJsonToXml, parseXmlFileToJson, xml2Json, @@ -111,7 +112,7 @@ export default class FlowTranslationProcessor extends BaseProcessor { // biome-ignore lint/suspicious/noExplicitAny: Any is expected here actualFlowDefinition: any ) { - const flowDefinitions = asArray( + const flowDefinitions = castArray( jsonTranslation.Translations?.flowDefinitions ) const fullNames = new Set( @@ -133,7 +134,7 @@ export default class FlowTranslationProcessor extends BaseProcessor { { path: translationPath, oid: this.config.to }, this.config ) - const flowDefinitions = asArray( + const flowDefinitions = castArray( translationJSON?.Translations?.flowDefinitions ) flowDefinitions.forEach(flowDefinition => diff --git a/src/service/inFileHandler.ts b/src/service/inFileHandler.ts index 15826b6b2..fe0adc203 100644 --- a/src/service/inFileHandler.ts +++ b/src/service/inFileHandler.ts @@ -24,7 +24,7 @@ export default class InFileHandler extends StandardHandler { ) { super(line, metadataDef, work, metadata) const inFileMetadata = getInFileAttributes(metadata) - this.metadataDiff = new MetadataDiff(this.config, metadata, inFileMetadata) + this.metadataDiff = new MetadataDiff(this.config, inFileMetadata) } public override async handleAddition() { diff --git a/src/service/objectTranslationHandler.ts b/src/service/objectTranslationHandler.ts index 79218c57d..872acaf0d 100644 --- a/src/service/objectTranslationHandler.ts +++ b/src/service/objectTranslationHandler.ts @@ -21,11 +21,7 @@ export default class ObjectTranslationHandler extends ResourceHandler { protected async _copyObjectTranslation(path: string) { const inFileMetadata = getInFileAttributes(this.metadata) - const metadataDiff = new MetadataDiff( - this.config, - this.metadata, - inFileMetadata - ) + const metadataDiff = new MetadataDiff(this.config, inFileMetadata) await metadataDiff.compare(path) const { xmlContent } = metadataDiff.prune() await writeFile(path, xmlContent, this.config) diff --git a/src/utils/fxpHelper.ts b/src/utils/fxpHelper.ts index 8bd504f81..d837ccd9d 100644 --- a/src/utils/fxpHelper.ts +++ b/src/utils/fxpHelper.ts @@ -25,10 +25,6 @@ const JSON_PARSER_OPTION = { suppressEmptyNode: false, } -export const asArray = (node: string[] | string) => { - return Array.isArray(node) ? node : [node] -} - export const xml2Json = (xmlContent: string) => { // biome-ignore lint/suspicious/noExplicitAny: Any is expected here let jsonContent: any = {} diff --git a/src/utils/metadataDiff.ts b/src/utils/metadataDiff.ts index 45c239492..508fb3c40 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -1,228 +1,233 @@ 'use strict' -import { isEqual } from 'lodash' - -import { MetadataRepository } from '../metadata/MetadataRepository' +import { castArray, differenceWith, isEqual, isUndefined } from 'lodash' import type { Config } from '../types/config' import type { SharedFileMetadata } from '../types/metadata' import type { Manifest } from '../types/work' - import { ATTRIBUTE_PREFIX, XML_HEADER_ATTRIBUTE_KEY, - asArray, convertJsonToXml, parseXmlFileToJson, } from './fxpHelper' import { fillPackageWithParameter } from './packageHelper' -type ManifestTypeMember = { - type: string - member: string +// biome-ignore lint/suspicious/noExplicitAny: +type XmlContent = Record + +type KeySelectorFn = (elem: XmlContent) => string | undefined + +interface DiffResult { + added: Manifest + deleted: Manifest } -// Store functional area -// Side effect on store -const addToStore = - (store: Manifest) => - ({ type, member }: ManifestTypeMember): Manifest => { - fillPackageWithParameter({ store, type, member }) - return store - } - -const hasMember = - (store: Manifest) => - (attributes: Map) => - (subType: string) => - (member: string) => - attributes.has(subType) && - store.get(attributes.get(subType)!.xmlName!)?.has(member) - -const selectKey = - (attributes: Map) => - (type: string) => - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (elem: any) => - elem?.[attributes.get(type)!.key!] - -// Metadata JSON structure functional area -// biome-ignore lint/suspicious/noExplicitAny: Any is expected here -const getRootMetadata = (fileContent: any): any => - fileContent[ - Object.keys(fileContent).find( - attribute => attribute !== XML_HEADER_ATTRIBUTE_KEY - ) as string - ] ?? {} - -const getSubTypeTags = - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (attributes: Map) => (fileContent: any) => - Object.keys(getRootMetadata(fileContent)).filter(tag => attributes.has(tag)) - -const extractMetadataForSubtype = - ( - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - fileContent: any - ) => - (subType: string): string[] => - asArray(getRootMetadata(fileContent)?.[subType]) - -// biome-ignore lint/suspicious/noExplicitAny: Any is expected here -const isEmpty = (fileContent: any) => - Object.entries(getRootMetadata(fileContent)) - .filter(([key]) => !key.startsWith(ATTRIBUTE_PREFIX)) - .every(([, value]) => Array.isArray(value) && value.length === 0) - -// Diff processing functional area -const compareContent = - (attributes: Map) => - ( - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - contentAtRef: any, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - otherContent: any, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - predicat: (arg0: any, arg1: string, arg2: string) => boolean - ): Manifest => { - const v: ManifestTypeMember[] = getSubTypeTags(attributes)( - contentAtRef - ).flatMap( - processMetadataForSubType( - contentAtRef, - otherContent, - predicat, - attributes - ) - ) - const store: Manifest = new Map() - v.forEach((nameByType: ManifestTypeMember) => addToStore(store)(nameByType)) - return store - } - -const processMetadataForSubType = - ( - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - baseContent: any, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - otherContent: any, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - predicat: (arg0: any, arg1: string, arg2: string) => boolean, +interface PrunedContent { + xmlContent: string + isEmpty: boolean +} + +export default class MetadataDiff { + private toContent!: XmlContent + private fromContent!: XmlContent + private extractor: MetadataExtractor + + constructor( + private config: Config, attributes: Map - ) => - (subType: string): ManifestTypeMember[] => { - const baseMeta = extractMetadataForSubtype(baseContent)(subType) - const otherMeta = extractMetadataForSubtype(otherContent)(subType) - const processElement = getElementProcessor( - subType, - predicat, - otherMeta, - attributes + ) { + this.extractor = new MetadataExtractor(attributes) + } + + async compare(path: string): Promise { + const [toContent, fromContent] = await Promise.all([ + parseXmlFileToJson({ path, oid: this.config.to }, this.config), + parseXmlFileToJson({ path, oid: this.config.from }, this.config), + ]) + + this.toContent = toContent + this.fromContent = fromContent + + const comparator = new MetadataComparator( + this.extractor, + this.fromContent, + this.toContent ) - return baseMeta - .map(processElement) - .filter(x => x !== undefined) as ManifestTypeMember[] + + const added = comparator.getChanges() + const deleted = comparator.getDeletion() + + return { added, deleted } } -const getElementProcessor = - ( - type: string, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - predicat: (arg0: any, arg1: string, arg2: string) => boolean, - otherMeta: string[], - attributes: Map - ) => - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (elem: any) => { - let metadataMember - if (predicat(otherMeta, type, elem)) { - metadataMember = { - type: attributes.get(type)!.xmlName, - member: selectKey(attributes)(type)(elem), - } + prune(): PrunedContent { + const transformer = new JsonTransformer(this.extractor) + const prunedContent = transformer.generatePartialJson( + this.fromContent, + this.toContent + ) + + return { + xmlContent: convertJsonToXml(prunedContent), + isEmpty: this.extractor.isContentEmpty(prunedContent), } - return metadataMember - } - -// Partial JSON generation functional area -// Side effect on jsonContent -const generatePartialJSON = - (attributes: Map) => - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (jsonContent: any) => - (store: Manifest) => { - const extract = extractMetadataForSubtype(jsonContent) - const storeHasMember = hasMember(store)(attributes) - return getSubTypeTags(attributes)(jsonContent).reduce((acc, subType) => { - const meta = extract(subType) - const storeHasMemberForType = storeHasMember(subType) - const key = selectKey(attributes)(subType) - const rootMetadata = getRootMetadata(acc) - rootMetadata[subType] = meta.filter(elem => - storeHasMemberForType(key(elem)) + } +} + +class MetadataExtractor { + constructor(readonly attributes: Map) {} + + getSubTypes(fileContent: XmlContent): string[] { + const root = this.extractRootElement(fileContent) + return Object.keys(root).filter(tag => this.attributes.has(tag)) + } + + isTypePackageable(subType: string): boolean { + return !this.attributes.get(subType)?.excluded + } + + getXmlName(subType: string): string { + return this.attributes.get(subType)?.xmlName! + } + + getKeyValueSelector(subType: string): KeySelectorFn { + const metadataKey = this.getKeyFieldDefinition(subType) + return elem => elem[metadataKey!] as string + } + + getKeyFieldDefinition(subType: string): string | undefined { + return this.attributes.get(subType)?.key + } + + extractForSubType(fileContent: XmlContent, subType: string): XmlContent[] { + const root = this.extractRootElement(fileContent) + const content = root[subType] + return content ? castArray(content) : [] + } + + isContentEmpty(fileContent: XmlContent): boolean { + const root = this.extractRootElement(fileContent) + return Object.entries(root) + .filter(([key]) => !key.startsWith(ATTRIBUTE_PREFIX)) + .every( + ([, value]) => !value || (Array.isArray(value) && value.length === 0) ) - return acc - }, structuredClone(jsonContent)) } -export default class MetadataDiff { - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - protected toContent: any - protected add!: Manifest + extractRootElement(fileContent: XmlContent): XmlContent { + const rootKey = + Object.keys(fileContent).find(key => key !== XML_HEADER_ATTRIBUTE_KEY) ?? + '' + return (fileContent[rootKey] as XmlContent) ?? {} + } +} + +class MetadataComparator { constructor( - protected readonly config: Config, - protected readonly metadata: MetadataRepository, - protected readonly attributes: Map + private extractor: MetadataExtractor, + private fromContent: XmlContent, + private toContent: XmlContent ) {} - public async compare(path: string) { - this.toContent = await parseXmlFileToJson( - { path, oid: this.config.to }, - this.config - ) - const fromContent = await parseXmlFileToJson( - { path, oid: this.config.from }, - this.config - ) + getChanges() { + return this.compare(this.toContent, this.fromContent, this.compareAdded) + } - const diff = compareContent(this.attributes) - - // Added or Modified - this.add = diff( - this.toContent, - fromContent, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (meta: any[], type: string, elem: string) => { - const key = selectKey(this.attributes)(type) - const match = meta.find((el: string) => key(el) === key(elem)) - return !match || !isEqual(match, elem) - } - ) + getDeletion() { + return this.compare(this.fromContent, this.toContent, this.compareDeleted) + } - // Will be done when not needed - // Deleted - const del = diff( - fromContent, - this.toContent, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (meta: any[], type: string, elem: string) => { - const key = selectKey(this.attributes)(type) - return !meta.some((el: string) => key(el) === key(elem)) - } - ) + private compare( + baseContent: XmlContent, + targetContent: XmlContent, + elementMatcher: ( + meta: XmlContent[], + keySelector: KeySelectorFn, + elem: XmlContent + ) => boolean + ): Manifest { + return this.extractor + .getSubTypes(baseContent) + .filter(subType => this.extractor.isTypePackageable(subType)) + .reduce((manifest, subType) => { + const baseMeta = this.extractor.extractForSubType(baseContent, subType) + const targetMeta = this.extractor.extractForSubType( + targetContent, + subType + ) + const keySelector = this.extractor.getKeyValueSelector(subType) + const xmlName = this.extractor.getXmlName(subType) + + baseMeta + .filter(elem => elementMatcher(targetMeta, keySelector, elem)) + .forEach(elem => { + fillPackageWithParameter({ + store: manifest, + type: xmlName, + member: keySelector(elem)!, + }) + }) + + return manifest + }, new Map()) + } - return { - added: this.add, - deleted: del, - } + private compareAdded = ( + meta: XmlContent[], + keySelector: KeySelectorFn, + elem: XmlContent + ) => { + const elemKey = keySelector(elem) + const match = meta.find(el => keySelector(el) === elemKey) + return !match || !isEqual(match, elem) } - public prune() { - const prunedContent = generatePartialJSON(this.attributes)(this.toContent)( - this.add - ) - return { - xmlContent: convertJsonToXml(prunedContent), - isEmpty: isEmpty(prunedContent), - } + private compareDeleted = ( + meta: XmlContent[], + keySelector: KeySelectorFn, + elem: XmlContent + ) => { + const elemKey = keySelector(elem) + return !meta.some(el => keySelector(el) === elemKey) + } +} + +class JsonTransformer { + constructor(private extractor: MetadataExtractor) {} + + generatePartialJson( + fromContent: XmlContent, + toContent: XmlContent + ): XmlContent { + return this.extractor.getSubTypes(toContent).reduce((acc, subType) => { + const fromMeta = this.extractor.extractForSubType(fromContent, subType) + const toMeta = this.extractor.extractForSubType(toContent, subType) + const keyField = this.extractor.getKeyFieldDefinition(subType) + + const partialContentBuilder = isUndefined(keyField) + ? this.getPartialContentWithoutKey + : this.getPartialContentWithKey + + this.extractor.extractRootElement(acc)[subType] = partialContentBuilder( + fromMeta, + toMeta + ) + + return acc + }, structuredClone(toContent)) + } + + private getPartialContentWithoutKey( + fromMeta: XmlContent[], + toMeta: XmlContent[] + ): XmlContent[] { + return isEqual(fromMeta, toMeta) ? [] : toMeta + } + + private getPartialContentWithKey( + fromMeta: XmlContent[], + toMeta: XmlContent[] + ): XmlContent[] { + return differenceWith(toMeta, fromMeta, isEqual) } }