From 4a34ec71909e1b5c14a5e914445ed28d4b74ff35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Sat, 30 Nov 2024 17:43:02 +0100 Subject: [PATCH 1/7] feat: add metadata definition for layout, ipLoginRange and loginHours --- src/metadata/a48.json | 30 ++++++++++++++++++++++++++++-- src/metadata/v46.json | 27 +++++++++++++++++++++++++++ src/metadata/v47.json | 27 +++++++++++++++++++++++++++ src/metadata/v49.json | 27 +++++++++++++++++++++++++++ src/metadata/v50.json | 27 +++++++++++++++++++++++++++ src/metadata/v51.json | 27 +++++++++++++++++++++++++++ src/metadata/v52.json | 27 +++++++++++++++++++++++++++ src/metadata/v53.json | 27 +++++++++++++++++++++++++++ src/metadata/v54.json | 27 +++++++++++++++++++++++++++ src/metadata/v55.json | 27 +++++++++++++++++++++++++++ src/metadata/v56.json | 27 +++++++++++++++++++++++++++ src/metadata/v57.json | 27 +++++++++++++++++++++++++++ src/metadata/v58.json | 27 +++++++++++++++++++++++++++ src/metadata/v59.json | 27 +++++++++++++++++++++++++++ src/metadata/v60.json | 27 +++++++++++++++++++++++++++ src/metadata/v61.json | 27 +++++++++++++++++++++++++++ src/metadata/v62.json | 27 +++++++++++++++++++++++++++ 17 files changed, 460 insertions(+), 2 deletions(-) diff --git a/src/metadata/a48.json b/src/metadata/a48.json index 5b8d001b..a6ac7b7f 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,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v46.json b/src/metadata/v46.json index 79f7043f..982a091c 100644 --- a/src/metadata/v46.json +++ b/src/metadata/v46.json @@ -1283,6 +1283,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v47.json b/src/metadata/v47.json index 5c392004..e93ea228 100644 --- a/src/metadata/v47.json +++ b/src/metadata/v47.json @@ -1515,6 +1515,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v49.json b/src/metadata/v49.json index 3228a8e1..2735e71b 100644 --- a/src/metadata/v49.json +++ b/src/metadata/v49.json @@ -1543,6 +1543,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v50.json b/src/metadata/v50.json index 813decf2..4678e4b2 100644 --- a/src/metadata/v50.json +++ b/src/metadata/v50.json @@ -1578,6 +1578,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v51.json b/src/metadata/v51.json index 2270d378..06bcffd4 100644 --- a/src/metadata/v51.json +++ b/src/metadata/v51.json @@ -1648,6 +1648,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v52.json b/src/metadata/v52.json index 21b75dd1..5580e78a 100644 --- a/src/metadata/v52.json +++ b/src/metadata/v52.json @@ -1655,6 +1655,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v53.json b/src/metadata/v53.json index 21b75dd1..5580e78a 100644 --- a/src/metadata/v53.json +++ b/src/metadata/v53.json @@ -1655,6 +1655,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v54.json b/src/metadata/v54.json index 3c8ea566..f75e0f10 100644 --- a/src/metadata/v54.json +++ b/src/metadata/v54.json @@ -1708,6 +1708,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v55.json b/src/metadata/v55.json index 5d10addc..58fc9048 100644 --- a/src/metadata/v55.json +++ b/src/metadata/v55.json @@ -1806,6 +1806,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v56.json b/src/metadata/v56.json index a94f767a..5a8ea3c7 100644 --- a/src/metadata/v56.json +++ b/src/metadata/v56.json @@ -1834,6 +1834,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v57.json b/src/metadata/v57.json index 1d5e9002..af6ca6c3 100644 --- a/src/metadata/v57.json +++ b/src/metadata/v57.json @@ -1883,6 +1883,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v58.json b/src/metadata/v58.json index 7960a37a..7555167f 100644 --- a/src/metadata/v58.json +++ b/src/metadata/v58.json @@ -1890,6 +1890,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v59.json b/src/metadata/v59.json index 1a9fb143..980ab61b 100644 --- a/src/metadata/v59.json +++ b/src/metadata/v59.json @@ -1912,6 +1912,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v60.json b/src/metadata/v60.json index 71e1639c..0f813ae1 100644 --- a/src/metadata/v60.json +++ b/src/metadata/v60.json @@ -1946,6 +1946,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v61.json b/src/metadata/v61.json index 31bc0c62..ec31f022 100644 --- a/src/metadata/v61.json +++ b/src/metadata/v61.json @@ -1953,6 +1953,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, diff --git a/src/metadata/v62.json b/src/metadata/v62.json index 31bc0c62..ec31f022 100644 --- a/src/metadata/v62.json +++ b/src/metadata/v62.json @@ -1953,6 +1953,33 @@ "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", + "key": "", + "excluded": true + }, + { + "inFolder": false, + "metaFile": false, + "parentXmlName": "Profile", + "xmlName": "ProfileLoginIpRange", + "xmlTag": "loginIpRanges", + "key": "", + "excluded": true + }, { "inFolder": false, "metaFile": false, From 2abe5a4e0adb063986b060ee230e76088666ba25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Sat, 30 Nov 2024 18:11:31 +0100 Subject: [PATCH 2/7] feat: implement logic for array and object comparison --- __tests__/unit/lib/utils/metadataDiff.test.ts | 249 +++++++++++++++++- src/utils/metadataDiff.ts | 77 ++++-- 2 files changed, 297 insertions(+), 29 deletions(-) diff --git a/__tests__/unit/lib/utils/metadataDiff.test.ts b/__tests__/unit/lib/utils/metadataDiff.test.ts index 5bcc7357..0a132e99 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,9 +199,11 @@ 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() @@ -116,7 +213,7 @@ describe.each([[{}], [xmlHeader]])(`MetadataDiff`, header => { metadataDiff = new MetadataDiff( work.config, globalMetadata, - workFlowAttributes + inFileAttribute ) }) @@ -291,6 +388,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/utils/metadataDiff.ts b/src/utils/metadataDiff.ts index 45c23949..693a2235 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -1,6 +1,6 @@ 'use strict' -import { isEqual } from 'lodash' +import { differenceWith, isEqual } from 'lodash' import { MetadataRepository } from '../metadata/MetadataRepository' import type { Config } from '../types/config' @@ -42,8 +42,13 @@ const selectKey = (attributes: Map) => (type: string) => // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (elem: any) => - elem?.[attributes.get(type)!.key!] + (elem: any): string => { + const key = attributes.get(type)?.key + return key ? elem?.[key] : null + } + +const isValid = (key: string) => + !['', '', null, undefined].includes(key) // Metadata JSON structure functional area // biome-ignore lint/suspicious/noExplicitAny: Any is expected here @@ -84,7 +89,7 @@ const compareContent = // biome-ignore lint/suspicious/noExplicitAny: Any is expected here predicat: (arg0: any, arg1: string, arg2: string) => boolean ): Manifest => { - const v: ManifestTypeMember[] = getSubTypeTags(attributes)( + const metadataMembers: ManifestTypeMember[] = getSubTypeTags(attributes)( contentAtRef ).flatMap( processMetadataForSubType( @@ -95,7 +100,9 @@ const compareContent = ) ) const store: Manifest = new Map() - v.forEach((nameByType: ManifestTypeMember) => addToStore(store)(nameByType)) + metadataMembers.forEach((nameByType: ManifestTypeMember) => + addToStore(store)(nameByType) + ) return store } @@ -144,29 +151,43 @@ const getElementProcessor = } // Partial JSON generation functional area -// Side effect on jsonContent const generatePartialJSON = (attributes: Map) => // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (jsonContent: any) => + (fromJsonContent: any) => + // biome-ignore lint/suspicious/noExplicitAny: Any is expected here + (toJsonContent: any) => (store: Manifest) => { - const extract = extractMetadataForSubtype(jsonContent) + const extract = extractMetadataForSubtype(toJsonContent) const storeHasMember = hasMember(store)(attributes) - return getSubTypeTags(attributes)(jsonContent).reduce((acc, subType) => { + const keySelectorAttribute = selectKey(attributes) + const fromExtractor = extractMetadataForSubtype(fromJsonContent) + return getSubTypeTags(attributes)(toJsonContent).reduce((acc, subType) => { + const fromMeta = fromExtractor(subType) const meta = extract(subType) const storeHasMemberForType = storeHasMember(subType) - const key = selectKey(attributes)(subType) + const keySelector = keySelectorAttribute(subType) const rootMetadata = getRootMetadata(acc) - rootMetadata[subType] = meta.filter(elem => - storeHasMemberForType(key(elem)) - ) + const keyField = attributes.get(subType)?.key + if (keyField === '') { + rootMetadata[subType] = isEqual(fromMeta, meta) ? [] : meta + } else if (keyField === '') { + rootMetadata[subType] = differenceWith(meta, fromMeta, isEqual) + } else { + rootMetadata[subType] = meta.filter(elem => { + const key = keySelector(elem) + return storeHasMemberForType(key) + }) + } return acc - }, structuredClone(jsonContent)) + }, structuredClone(toJsonContent)) } export default class MetadataDiff { // biome-ignore lint/suspicious/noExplicitAny: Any is expected here protected toContent: any + // biome-ignore lint/suspicious/noExplicitAny: Any is expected here + protected fromContent: any protected add!: Manifest constructor( protected readonly config: Config, @@ -179,21 +200,26 @@ export default class MetadataDiff { { path, oid: this.config.to }, this.config ) - const fromContent = await parseXmlFileToJson( + this.fromContent = await parseXmlFileToJson( { path, oid: this.config.from }, this.config ) const diff = compareContent(this.attributes) + const keySelector = selectKey(this.attributes) + // Added or Modified this.add = diff( this.toContent, - fromContent, + this.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)) + const key = keySelector(type) + const elemKey = key(elem) + const match = isValid(elemKey) + ? meta.find((el: string) => key(el) === elemKey) + : null return !match || !isEqual(match, elem) } ) @@ -201,12 +227,15 @@ export default class MetadataDiff { // Will be done when not needed // Deleted const del = diff( - fromContent, + this.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)) + const key = keySelector(type) + const elemKey = key(elem) + return ( + isValid(elemKey) && !meta.some((el: string) => key(el) === elemKey) + ) } ) @@ -217,9 +246,9 @@ export default class MetadataDiff { } public prune() { - const prunedContent = generatePartialJSON(this.attributes)(this.toContent)( - this.add - ) + const prunedContent = generatePartialJSON(this.attributes)( + this.fromContent + )(this.toContent)(this.add) return { xmlContent: convertJsonToXml(prunedContent), isEmpty: isEmpty(prunedContent), From 76507096a599861ff79db57264ddab735112dced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Sat, 30 Nov 2024 18:32:45 +0100 Subject: [PATCH 3/7] refactor: simplify standard case comparison algorithm --- src/metadata/v46.json | 2 +- src/metadata/v47.json | 2 +- src/metadata/v49.json | 2 +- src/metadata/v50.json | 2 +- src/metadata/v51.json | 2 +- src/metadata/v52.json | 2 +- src/metadata/v53.json | 2 +- src/metadata/v54.json | 2 +- src/metadata/v55.json | 2 +- src/metadata/v56.json | 2 +- src/metadata/v57.json | 2 +- src/metadata/v58.json | 2 +- src/metadata/v59.json | 2 +- src/metadata/v60.json | 2 +- src/metadata/v61.json | 2 +- src/metadata/v62.json | 2 +- src/utils/metadataDiff.ts | 30 +++++------------------------- 17 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/metadata/v46.json b/src/metadata/v46.json index 982a091c..9b3c4da8 100644 --- a/src/metadata/v46.json +++ b/src/metadata/v46.json @@ -1289,7 +1289,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v47.json b/src/metadata/v47.json index e93ea228..08c99964 100644 --- a/src/metadata/v47.json +++ b/src/metadata/v47.json @@ -1521,7 +1521,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v49.json b/src/metadata/v49.json index 2735e71b..867ab8ac 100644 --- a/src/metadata/v49.json +++ b/src/metadata/v49.json @@ -1549,7 +1549,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v50.json b/src/metadata/v50.json index 4678e4b2..be3e3ba1 100644 --- a/src/metadata/v50.json +++ b/src/metadata/v50.json @@ -1584,7 +1584,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v51.json b/src/metadata/v51.json index 06bcffd4..c0a2b361 100644 --- a/src/metadata/v51.json +++ b/src/metadata/v51.json @@ -1654,7 +1654,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v52.json b/src/metadata/v52.json index 5580e78a..f1f78066 100644 --- a/src/metadata/v52.json +++ b/src/metadata/v52.json @@ -1661,7 +1661,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v53.json b/src/metadata/v53.json index 5580e78a..f1f78066 100644 --- a/src/metadata/v53.json +++ b/src/metadata/v53.json @@ -1661,7 +1661,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v54.json b/src/metadata/v54.json index f75e0f10..75131e0d 100644 --- a/src/metadata/v54.json +++ b/src/metadata/v54.json @@ -1714,7 +1714,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v55.json b/src/metadata/v55.json index 58fc9048..7703561f 100644 --- a/src/metadata/v55.json +++ b/src/metadata/v55.json @@ -1812,7 +1812,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v56.json b/src/metadata/v56.json index 5a8ea3c7..07084fbf 100644 --- a/src/metadata/v56.json +++ b/src/metadata/v56.json @@ -1840,7 +1840,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v57.json b/src/metadata/v57.json index af6ca6c3..c4c380e6 100644 --- a/src/metadata/v57.json +++ b/src/metadata/v57.json @@ -1889,7 +1889,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v58.json b/src/metadata/v58.json index 7555167f..2381c2b9 100644 --- a/src/metadata/v58.json +++ b/src/metadata/v58.json @@ -1896,7 +1896,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v59.json b/src/metadata/v59.json index 980ab61b..acd7e080 100644 --- a/src/metadata/v59.json +++ b/src/metadata/v59.json @@ -1918,7 +1918,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v60.json b/src/metadata/v60.json index 0f813ae1..a8c872ca 100644 --- a/src/metadata/v60.json +++ b/src/metadata/v60.json @@ -1952,7 +1952,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v61.json b/src/metadata/v61.json index ec31f022..d3e78b1d 100644 --- a/src/metadata/v61.json +++ b/src/metadata/v61.json @@ -1959,7 +1959,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/metadata/v62.json b/src/metadata/v62.json index ec31f022..d3e78b1d 100644 --- a/src/metadata/v62.json +++ b/src/metadata/v62.json @@ -1959,7 +1959,7 @@ "parentXmlName": "Profile", "xmlName": "ProfileLayoutAssignments", "xmlTag": "layoutAssignments", - "key": "", + "key": "layout", "excluded": true }, { diff --git a/src/utils/metadataDiff.ts b/src/utils/metadataDiff.ts index 693a2235..8f4fff5e 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -30,22 +30,12 @@ const addToStore = 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): string => { - const key = attributes.get(type)?.key - return key ? elem?.[key] : null - } + (elem: any): string => + elem?.[attributes.get(type)?.key!] const isValid = (key: string) => !['', '', null, undefined].includes(key) @@ -156,28 +146,18 @@ const generatePartialJSON = // biome-ignore lint/suspicious/noExplicitAny: Any is expected here (fromJsonContent: any) => // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (toJsonContent: any) => - (store: Manifest) => { + (toJsonContent: any) => { const extract = extractMetadataForSubtype(toJsonContent) - const storeHasMember = hasMember(store)(attributes) - const keySelectorAttribute = selectKey(attributes) const fromExtractor = extractMetadataForSubtype(fromJsonContent) return getSubTypeTags(attributes)(toJsonContent).reduce((acc, subType) => { const fromMeta = fromExtractor(subType) const meta = extract(subType) - const storeHasMemberForType = storeHasMember(subType) - const keySelector = keySelectorAttribute(subType) const rootMetadata = getRootMetadata(acc) const keyField = attributes.get(subType)?.key if (keyField === '') { rootMetadata[subType] = isEqual(fromMeta, meta) ? [] : meta - } else if (keyField === '') { - rootMetadata[subType] = differenceWith(meta, fromMeta, isEqual) } else { - rootMetadata[subType] = meta.filter(elem => { - const key = keySelector(elem) - return storeHasMemberForType(key) - }) + rootMetadata[subType] = differenceWith(meta, fromMeta, isEqual) } return acc }, structuredClone(toJsonContent)) @@ -248,7 +228,7 @@ export default class MetadataDiff { public prune() { const prunedContent = generatePartialJSON(this.attributes)( this.fromContent - )(this.toContent)(this.add) + )(this.toContent) return { xmlContent: convertJsonToXml(prunedContent), isEmpty: isEmpty(prunedContent), From 60b1c88d05d3af2a4375431112d453dfe205d02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Sun, 1 Dec 2024 14:03:06 +0100 Subject: [PATCH 4/7] refactor: move to OO implementation instead of functional --- __tests__/unit/lib/utils/metadataDiff.test.ts | 6 +- src/metadata/a48.json | 2 - src/metadata/v46.json | 2 - src/metadata/v47.json | 2 - src/metadata/v49.json | 2 - src/metadata/v50.json | 2 - src/metadata/v51.json | 2 - src/metadata/v52.json | 2 - src/metadata/v53.json | 2 - src/metadata/v54.json | 2 - src/metadata/v55.json | 2 - src/metadata/v56.json | 2 - src/metadata/v57.json | 2 - src/metadata/v58.json | 2 - src/metadata/v59.json | 2 - src/metadata/v60.json | 2 - src/metadata/v61.json | 2 - src/metadata/v62.json | 2 - src/service/inFileHandler.ts | 2 +- src/service/objectTranslationHandler.ts | 6 +- src/utils/metadataDiff.ts | 329 ++++++++---------- 21 files changed, 154 insertions(+), 223 deletions(-) diff --git a/__tests__/unit/lib/utils/metadataDiff.test.ts b/__tests__/unit/lib/utils/metadataDiff.test.ts index 0a132e99..46c168ff 100644 --- a/__tests__/unit/lib/utils/metadataDiff.test.ts +++ b/__tests__/unit/lib/utils/metadataDiff.test.ts @@ -210,11 +210,7 @@ describe.each([[{}], [xmlHeader]])(`MetadataDiff`, header => { work = getWork() work.config.from = 'from' work.config.to = 'to' - metadataDiff = new MetadataDiff( - work.config, - globalMetadata, - inFileAttribute - ) + metadataDiff = new MetadataDiff(work.config, inFileAttribute) }) describe(`compare with ${JSON.stringify(header)} header`, () => { diff --git a/src/metadata/a48.json b/src/metadata/a48.json index a6ac7b7f..4786d7ac 100644 --- a/src/metadata/a48.json +++ b/src/metadata/a48.json @@ -1529,7 +1529,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1538,7 +1537,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v46.json b/src/metadata/v46.json index 9b3c4da8..3b35e17a 100644 --- a/src/metadata/v46.json +++ b/src/metadata/v46.json @@ -1298,7 +1298,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1307,7 +1306,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v47.json b/src/metadata/v47.json index 08c99964..8817d8e4 100644 --- a/src/metadata/v47.json +++ b/src/metadata/v47.json @@ -1530,7 +1530,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1539,7 +1538,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v49.json b/src/metadata/v49.json index 867ab8ac..a62eaae6 100644 --- a/src/metadata/v49.json +++ b/src/metadata/v49.json @@ -1558,7 +1558,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1567,7 +1566,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v50.json b/src/metadata/v50.json index be3e3ba1..c5f6137d 100644 --- a/src/metadata/v50.json +++ b/src/metadata/v50.json @@ -1593,7 +1593,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1602,7 +1601,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v51.json b/src/metadata/v51.json index c0a2b361..5e4291ba 100644 --- a/src/metadata/v51.json +++ b/src/metadata/v51.json @@ -1663,7 +1663,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1672,7 +1671,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v52.json b/src/metadata/v52.json index f1f78066..23ec2487 100644 --- a/src/metadata/v52.json +++ b/src/metadata/v52.json @@ -1670,7 +1670,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1679,7 +1678,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v53.json b/src/metadata/v53.json index f1f78066..23ec2487 100644 --- a/src/metadata/v53.json +++ b/src/metadata/v53.json @@ -1670,7 +1670,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1679,7 +1678,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v54.json b/src/metadata/v54.json index 75131e0d..467488c9 100644 --- a/src/metadata/v54.json +++ b/src/metadata/v54.json @@ -1723,7 +1723,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1732,7 +1731,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v55.json b/src/metadata/v55.json index 7703561f..81971871 100644 --- a/src/metadata/v55.json +++ b/src/metadata/v55.json @@ -1821,7 +1821,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1830,7 +1829,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v56.json b/src/metadata/v56.json index 07084fbf..84f97b4c 100644 --- a/src/metadata/v56.json +++ b/src/metadata/v56.json @@ -1849,7 +1849,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1858,7 +1857,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v57.json b/src/metadata/v57.json index c4c380e6..88eb1567 100644 --- a/src/metadata/v57.json +++ b/src/metadata/v57.json @@ -1898,7 +1898,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1907,7 +1906,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v58.json b/src/metadata/v58.json index 2381c2b9..73a05353 100644 --- a/src/metadata/v58.json +++ b/src/metadata/v58.json @@ -1905,7 +1905,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1914,7 +1913,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v59.json b/src/metadata/v59.json index acd7e080..5f549b4a 100644 --- a/src/metadata/v59.json +++ b/src/metadata/v59.json @@ -1927,7 +1927,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1936,7 +1935,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v60.json b/src/metadata/v60.json index a8c872ca..13c896bf 100644 --- a/src/metadata/v60.json +++ b/src/metadata/v60.json @@ -1961,7 +1961,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1970,7 +1969,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v61.json b/src/metadata/v61.json index d3e78b1d..8cb06d90 100644 --- a/src/metadata/v61.json +++ b/src/metadata/v61.json @@ -1968,7 +1968,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1977,7 +1976,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/metadata/v62.json b/src/metadata/v62.json index d3e78b1d..8cb06d90 100644 --- a/src/metadata/v62.json +++ b/src/metadata/v62.json @@ -1968,7 +1968,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginHours", "xmlTag": "loginHours", - "key": "", "excluded": true }, { @@ -1977,7 +1976,6 @@ "parentXmlName": "Profile", "xmlName": "ProfileLoginIpRange", "xmlTag": "loginIpRanges", - "key": "", "excluded": true }, { diff --git a/src/service/inFileHandler.ts b/src/service/inFileHandler.ts index 15826b6b..fe0adc20 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 79218c57..872acaf0 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/metadataDiff.ts b/src/utils/metadataDiff.ts index 8f4fff5e..3b5b7c53 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -1,8 +1,7 @@ 'use strict' -import { differenceWith, isEqual } from 'lodash' +import { differenceWith, isEqual, isUndefined } from 'lodash' -import { MetadataRepository } from '../metadata/MetadataRepository' import type { Config } from '../types/config' import type { SharedFileMetadata } from '../types/metadata' import type { Manifest } from '../types/work' @@ -16,166 +15,148 @@ import { } from './fxpHelper' import { fillPackageWithParameter } from './packageHelper' -type ManifestTypeMember = { - type: string - member: string +type 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 - } +type PrunedContent = { + xmlContent: string + isEmpty: boolean +} -const selectKey = - (attributes: Map) => - (type: string) => - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (elem: any): string => - elem?.[attributes.get(type)?.key!] - -const isValid = (key: string) => - !['', '', null, undefined].includes(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 metadataMembers: ManifestTypeMember[] = getSubTypeTags(attributes)( - contentAtRef - ).flatMap( - processMetadataForSubType( - contentAtRef, - otherContent, - predicat, - attributes - ) - ) - const store: Manifest = new Map() - metadataMembers.forEach((nameByType: ManifestTypeMember) => - addToStore(store)(nameByType) +class MetadataExtractor { + constructor(private attributes: Map) {} + + // biome-ignore lint/suspicious/noExplicitAny: + public getRoot(fileContent: any): any { + return ( + fileContent[ + Object.keys(fileContent).find( + attr => attr !== XML_HEADER_ATTRIBUTE_KEY + )! + ] ?? {} ) - 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, - attributes: Map - ) => - (subType: string): ManifestTypeMember[] => { - const baseMeta = extractMetadataForSubtype(baseContent)(subType) - const otherMeta = extractMetadataForSubtype(otherContent)(subType) - const processElement = getElementProcessor( - subType, - predicat, - otherMeta, - attributes - ) - return baseMeta - .map(processElement) - .filter(x => x !== undefined) as ManifestTypeMember[] + isTypePackageable(subType: string): unknown { + return this.attributes.get(subType)?.excluded !== true } -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), - } - } - return metadataMember + // biome-ignore lint/suspicious/noExplicitAny: + public getSubTypes(fileContent: any): string[] { + const root = this.getRoot(fileContent) + return Object.keys(root).filter(tag => this.attributes.has(tag)) } -// Partial JSON generation functional area -const generatePartialJSON = - (attributes: Map) => - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (fromJsonContent: any) => - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (toJsonContent: any) => { - const extract = extractMetadataForSubtype(toJsonContent) - const fromExtractor = extractMetadataForSubtype(fromJsonContent) - return getSubTypeTags(attributes)(toJsonContent).reduce((acc, subType) => { - const fromMeta = fromExtractor(subType) - const meta = extract(subType) - const rootMetadata = getRootMetadata(acc) - const keyField = attributes.get(subType)?.key - if (keyField === '') { - rootMetadata[subType] = isEqual(fromMeta, meta) ? [] : meta - } else { - rootMetadata[subType] = differenceWith(meta, fromMeta, isEqual) - } + getXmlName(subType: string) { + return this.attributes.get(subType)?.xmlName! + } + + public getKeySelector(subType: string) { + // biome-ignore lint/suspicious/noExplicitAny: + return (elem: any) => elem[this.attributes.get(subType)?.key!] + } + + // biome-ignore lint/suspicious/noExplicitAny: + public extractForSubType(fileContent: any, subType: string): any[] { + return asArray(this.getRoot(fileContent)?.[subType] ?? []) + } + + // biome-ignore lint/suspicious/noExplicitAny: + public isContentEmpty(fileContent: any): boolean { + const root = this.getRoot(fileContent) + return Object.entries(root) + .filter(([key]) => !key.startsWith(ATTRIBUTE_PREFIX)) + .every(([, value]) => Array.isArray(value) && value.length === 0) + } +} + +class MetadataComparator { + constructor(private extractor: MetadataExtractor) {} + + public compare( + // biome-ignore lint/suspicious/noExplicitAny: + baseContent: any, + // biome-ignore lint/suspicious/noExplicitAny: + targetContent: any, + // biome-ignore lint/suspicious/noExplicitAny: + predicate: (base: any[], type: string, key: string) => boolean + ): Manifest { + const subTypes = this.extractor.getSubTypes(baseContent) + + return subTypes + .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.getKeySelector(subType) + const xmlName = this.extractor.getXmlName(subType) + for (const elem of baseMeta) { + if (predicate(targetMeta, subType, elem)) { + fillPackageWithParameter({ + store: manifest, + type: xmlName, + member: keySelector(elem), + }) + } + } + return manifest + }, new Map()) + } +} + +class JsonTransformer { + constructor(private attributes: Map) {} + + // biome-ignore lint/suspicious/noExplicitAny: + public generatePartialJson(fromContent: any, toContent: any): any { + const metadataExtractor = new MetadataExtractor(this.attributes) + const subTypes = metadataExtractor.getSubTypes(toContent) + return subTypes.reduce((acc, subType) => { + const fromMeta = metadataExtractor.extractForSubType(fromContent, subType) + const toMeta = metadataExtractor.extractForSubType(toContent, subType) + + const rootMetadata = metadataExtractor.getRoot(acc) + + rootMetadata[subType] = this.getPartialContent(fromMeta, toMeta, subType) return acc - }, structuredClone(toJsonContent)) + }, structuredClone(toContent)) } + private getPartialContent( + fromMeta: string[], + toMeta: string[], + subType: string + ): string[] { + const keyField = this.attributes.get(subType)?.key + if (isUndefined(keyField)) { + return isEqual(fromMeta, toMeta) ? [] : toMeta + } + return differenceWith(toMeta, fromMeta, isEqual) + } +} + export default class MetadataDiff { - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - protected toContent: any - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - protected fromContent: any - protected add!: Manifest + // biome-ignore lint/suspicious/noExplicitAny: + private toContent: any + // biome-ignore lint/suspicious/noExplicitAny: + private fromContent: any + private added!: Manifest + private metadataExtractor!: MetadataExtractor + constructor( - protected readonly config: Config, - protected readonly metadata: MetadataRepository, - protected readonly attributes: Map - ) {} + private config: Config, + private attributes: Map + ) { + this.metadataExtractor = new MetadataExtractor(this.attributes) + } - public async compare(path: string) { + public async compare(path: string): Promise { this.toContent = await parseXmlFileToJson( { path, oid: this.config.to }, this.config @@ -185,53 +166,45 @@ export default class MetadataDiff { this.config ) - const diff = compareContent(this.attributes) - - const keySelector = selectKey(this.attributes) - - // Added or Modified - this.add = diff( + const comparator = new MetadataComparator(this.metadataExtractor) + this.added = comparator.compare( this.toContent, this.fromContent, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (meta: any[], type: string, elem: string) => { - const key = keySelector(type) - const elemKey = key(elem) - const match = isValid(elemKey) - ? meta.find((el: string) => key(el) === elemKey) - : null + // biome-ignore lint/suspicious/noExplicitAny: + (meta, type, elem: any) => { + const keySelector = this.metadataExtractor.getKeySelector(type) + const elemKey = keySelector(elem) + // biome-ignore lint/suspicious/noExplicitAny: + const match = meta.find((el: any) => keySelector(el) === elemKey) return !match || !isEqual(match, elem) } ) - // Will be done when not needed - // Deleted - const del = diff( + const deleted = comparator.compare( this.fromContent, this.toContent, - // biome-ignore lint/suspicious/noExplicitAny: Any is expected here - (meta: any[], type: string, elem: string) => { - const key = keySelector(type) - const elemKey = key(elem) - return ( - isValid(elemKey) && !meta.some((el: string) => key(el) === elemKey) - ) + // biome-ignore lint/suspicious/noExplicitAny: + (meta, type, elem: any) => { + const keySelector = this.metadataExtractor.getKeySelector(type) + const elemKey = keySelector(elem) + // biome-ignore lint/suspicious/noExplicitAny: + return !meta.some((el: any) => keySelector(el) === elemKey) } ) - return { - added: this.add, - deleted: del, - } + return { added: this.added, deleted } } - public prune() { - const prunedContent = generatePartialJSON(this.attributes)( - this.fromContent - )(this.toContent) + public prune(): PrunedContent { + const transformer = new JsonTransformer(this.attributes) + const prunedContent = transformer.generatePartialJson( + this.fromContent, + this.toContent + ) + return { xmlContent: convertJsonToXml(prunedContent), - isEmpty: isEmpty(prunedContent), + isEmpty: this.metadataExtractor.isContentEmpty(prunedContent), } } } From a8a60689c0f32bd14ca2e1d9fe96fbcfaa00e149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Sun, 1 Dec 2024 19:46:10 +0100 Subject: [PATCH 5/7] refactor: simplify implementation and improve performance --- src/utils/fxpHelper.ts | 3 +- src/utils/metadataDiff.ts | 288 ++++++++++++++++++++------------------ 2 files changed, 157 insertions(+), 134 deletions(-) diff --git a/src/utils/fxpHelper.ts b/src/utils/fxpHelper.ts index 8bd504f8..7375f80f 100644 --- a/src/utils/fxpHelper.ts +++ b/src/utils/fxpHelper.ts @@ -25,7 +25,8 @@ const JSON_PARSER_OPTION = { suppressEmptyNode: false, } -export const asArray = (node: string[] | string) => { +// biome-ignore lint/suspicious/noExplicitAny: +export const asArray = (node: any[] | any) => { return Array.isArray(node) ? node : [node] } diff --git a/src/utils/metadataDiff.ts b/src/utils/metadataDiff.ts index 3b5b7c53..aba2a55c 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -1,11 +1,9 @@ 'use strict' import { 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, @@ -15,77 +13,159 @@ import { } from './fxpHelper' import { fillPackageWithParameter } from './packageHelper' -type DiffResult = { +// biome-ignore lint/suspicious/noExplicitAny: +type XmlContent = Record +// biome-ignore lint/suspicious/noExplicitAny: +type XmlElement = Record + +type KeySelectorFn = (elem: XmlElement) => string | undefined + +interface DiffResult { added: Manifest deleted: Manifest } -type PrunedContent = { +interface PrunedContent { xmlContent: string isEmpty: boolean } -class MetadataExtractor { - constructor(private attributes: Map) {} +export default class MetadataDiff { + private toContent!: XmlContent + private fromContent!: XmlContent + private extractor: MetadataExtractor - // biome-ignore lint/suspicious/noExplicitAny: - public getRoot(fileContent: any): any { - return ( - fileContent[ - Object.keys(fileContent).find( - attr => attr !== XML_HEADER_ATTRIBUTE_KEY - )! - ] ?? {} + constructor( + private config: Config, + attributes: Map + ) { + 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) + + const added = comparator.compare( + this.toContent, + this.fromContent, + this.compareAdded() ) + const deleted = comparator.compare( + this.fromContent, + this.toContent, + this.compareDeleted() + ) + + return { added, deleted } } - isTypePackageable(subType: string): unknown { - return this.attributes.get(subType)?.excluded !== true + 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), + } } - // biome-ignore lint/suspicious/noExplicitAny: - public getSubTypes(fileContent: any): string[] { - const root = this.getRoot(fileContent) + private compareAdded() { + return ( + meta: XmlElement[], + keySelector: KeySelectorFn, + elem: XmlElement + ) => { + const elemKey = keySelector(elem) + const match = meta.find(el => keySelector(el) === elemKey) + return !match || !isEqual(match, elem) + } + } + + private compareDeleted() { + return ( + meta: XmlElement[], + keySelector: KeySelectorFn, + elem: XmlElement + ) => { + const elemKey = keySelector(elem) + return !meta.some(el => keySelector(el) === elemKey) + } + } +} + +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)) } - getXmlName(subType: string) { + isTypePackageable(subType: string): boolean { + return !this.attributes.get(subType)?.excluded + } + + getXmlName(subType: string): string { return this.attributes.get(subType)?.xmlName! } - public getKeySelector(subType: string) { - // biome-ignore lint/suspicious/noExplicitAny: - return (elem: any) => elem[this.attributes.get(subType)?.key!] + getKeyValueSelector(subType: string): KeySelectorFn { + const metadataKey = this.getKeyFieldDefinition(subType) + return elem => elem[metadataKey!] as string } - // biome-ignore lint/suspicious/noExplicitAny: - public extractForSubType(fileContent: any, subType: string): any[] { - return asArray(this.getRoot(fileContent)?.[subType] ?? []) + getKeyFieldDefinition(subType: string): string | undefined { + return this.attributes.get(subType)?.key } - // biome-ignore lint/suspicious/noExplicitAny: - public isContentEmpty(fileContent: any): boolean { - const root = this.getRoot(fileContent) + extractForSubType(fileContent: XmlContent, subType: string): XmlElement[] { + const root = this.extractRootElement(fileContent) + const content = root[subType] + return content ? asArray(content) : [] + } + + isContentEmpty(fileContent: XmlContent): boolean { + const root = this.extractRootElement(fileContent) return Object.entries(root) .filter(([key]) => !key.startsWith(ATTRIBUTE_PREFIX)) - .every(([, value]) => Array.isArray(value) && value.length === 0) + .every( + ([, value]) => !value || (Array.isArray(value) && value.length === 0) + ) + } + + extractRootElement(fileContent: XmlContent): XmlElement { + const rootKey = + Object.keys(fileContent).find(key => key !== XML_HEADER_ATTRIBUTE_KEY) ?? + '' + return (fileContent[rootKey] as XmlElement) ?? {} } } class MetadataComparator { constructor(private extractor: MetadataExtractor) {} - public compare( - // biome-ignore lint/suspicious/noExplicitAny: - baseContent: any, - // biome-ignore lint/suspicious/noExplicitAny: - targetContent: any, - // biome-ignore lint/suspicious/noExplicitAny: - predicate: (base: any[], type: string, key: string) => boolean + compare( + baseContent: XmlContent, + targetContent: XmlContent, + elementMatcher: ( + meta: XmlElement[], + keySelector: KeySelectorFn, + elem: XmlElement + ) => boolean ): Manifest { - const subTypes = this.extractor.getSubTypes(baseContent) - - return subTypes + return this.extractor + .getSubTypes(baseContent) .filter(subType => this.extractor.isTypePackageable(subType)) .reduce((manifest, subType) => { const baseMeta = this.extractor.extractForSubType(baseContent, subType) @@ -93,118 +173,60 @@ class MetadataComparator { targetContent, subType ) - - const keySelector = this.extractor.getKeySelector(subType) + const keySelector = this.extractor.getKeyValueSelector(subType) const xmlName = this.extractor.getXmlName(subType) - for (const elem of baseMeta) { - if (predicate(targetMeta, subType, elem)) { + + baseMeta + .filter(elem => elementMatcher(targetMeta, keySelector, elem)) + .forEach(elem => { fillPackageWithParameter({ store: manifest, type: xmlName, - member: keySelector(elem), + member: keySelector(elem)!, }) - } - } + }) + return manifest }, new Map()) } } class JsonTransformer { - constructor(private attributes: Map) {} + constructor(private extractor: MetadataExtractor) {} - // biome-ignore lint/suspicious/noExplicitAny: - public generatePartialJson(fromContent: any, toContent: any): any { - const metadataExtractor = new MetadataExtractor(this.attributes) - const subTypes = metadataExtractor.getSubTypes(toContent) - return subTypes.reduce((acc, subType) => { - const fromMeta = metadataExtractor.extractForSubType(fromContent, subType) - const toMeta = metadataExtractor.extractForSubType(toContent, subType) + 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 rootMetadata = metadataExtractor.getRoot(acc) + const partialContentBuilder = isUndefined(keyField) + ? this.getPartialContentWithoutKey + : this.getPartialContentWithKey + + this.extractor.extractRootElement(acc)[subType] = partialContentBuilder( + fromMeta, + toMeta + ) - rootMetadata[subType] = this.getPartialContent(fromMeta, toMeta, subType) return acc }, structuredClone(toContent)) } - private getPartialContent( - fromMeta: string[], - toMeta: string[], - subType: string - ): string[] { - const keyField = this.attributes.get(subType)?.key - if (isUndefined(keyField)) { - return isEqual(fromMeta, toMeta) ? [] : toMeta - } - return differenceWith(toMeta, fromMeta, isEqual) - } -} - -export default class MetadataDiff { - // biome-ignore lint/suspicious/noExplicitAny: - private toContent: any - // biome-ignore lint/suspicious/noExplicitAny: - private fromContent: any - private added!: Manifest - private metadataExtractor!: MetadataExtractor - - constructor( - private config: Config, - private attributes: Map - ) { - this.metadataExtractor = new MetadataExtractor(this.attributes) + private getPartialContentWithoutKey( + fromMeta: XmlElement[], + toMeta: XmlElement[] + ): XmlElement[] { + return isEqual(fromMeta, toMeta) ? [] : toMeta } - public async compare(path: string): Promise { - this.toContent = await parseXmlFileToJson( - { path, oid: this.config.to }, - this.config - ) - this.fromContent = await parseXmlFileToJson( - { path, oid: this.config.from }, - this.config - ) - - const comparator = new MetadataComparator(this.metadataExtractor) - this.added = comparator.compare( - this.toContent, - this.fromContent, - // biome-ignore lint/suspicious/noExplicitAny: - (meta, type, elem: any) => { - const keySelector = this.metadataExtractor.getKeySelector(type) - const elemKey = keySelector(elem) - // biome-ignore lint/suspicious/noExplicitAny: - const match = meta.find((el: any) => keySelector(el) === elemKey) - return !match || !isEqual(match, elem) - } - ) - - const deleted = comparator.compare( - this.fromContent, - this.toContent, - // biome-ignore lint/suspicious/noExplicitAny: - (meta, type, elem: any) => { - const keySelector = this.metadataExtractor.getKeySelector(type) - const elemKey = keySelector(elem) - // biome-ignore lint/suspicious/noExplicitAny: - return !meta.some((el: any) => keySelector(el) === elemKey) - } - ) - - return { added: this.added, deleted } - } - - public prune(): PrunedContent { - const transformer = new JsonTransformer(this.attributes) - const prunedContent = transformer.generatePartialJson( - this.fromContent, - this.toContent - ) - - return { - xmlContent: convertJsonToXml(prunedContent), - isEmpty: this.metadataExtractor.isContentEmpty(prunedContent), - } + private getPartialContentWithKey( + fromMeta: XmlElement[], + toMeta: XmlElement[] + ): XmlElement[] { + return differenceWith(toMeta, fromMeta, isEqual) } } From 346e7449c8c80283ad261258b8753da7ef57dbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Mon, 2 Dec 2024 11:14:35 +0100 Subject: [PATCH 6/7] refactor: replace asArray with lodash.castArray --- __tests__/unit/lib/utils/fxpHelper.test.ts | 27 ------------- .../flowTranslationProcessor.ts | 7 ++-- src/utils/fxpHelper.ts | 5 --- src/utils/metadataDiff.ts | 39 +++++++++---------- 4 files changed, 22 insertions(+), 56 deletions(-) diff --git a/__tests__/unit/lib/utils/fxpHelper.test.ts b/__tests__/unit/lib/utils/fxpHelper.test.ts index da82f9f7..d47bdf6d 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/src/post-processor/flowTranslationProcessor.ts b/src/post-processor/flowTranslationProcessor.ts index 764a7eac..4b92c467 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/utils/fxpHelper.ts b/src/utils/fxpHelper.ts index 7375f80f..d837ccd9 100644 --- a/src/utils/fxpHelper.ts +++ b/src/utils/fxpHelper.ts @@ -25,11 +25,6 @@ const JSON_PARSER_OPTION = { suppressEmptyNode: false, } -// biome-ignore lint/suspicious/noExplicitAny: -export const asArray = (node: any[] | any) => { - 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 aba2a55c..ad935333 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -1,13 +1,12 @@ 'use strict' -import { differenceWith, isEqual, isUndefined } from 'lodash' +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' @@ -15,10 +14,8 @@ import { fillPackageWithParameter } from './packageHelper' // biome-ignore lint/suspicious/noExplicitAny: type XmlContent = Record -// biome-ignore lint/suspicious/noExplicitAny: -type XmlElement = Record -type KeySelectorFn = (elem: XmlElement) => string | undefined +type KeySelectorFn = (elem: XmlContent) => string | undefined interface DiffResult { added: Manifest @@ -82,9 +79,9 @@ export default class MetadataDiff { private compareAdded() { return ( - meta: XmlElement[], + meta: XmlContent[], keySelector: KeySelectorFn, - elem: XmlElement + elem: XmlContent ) => { const elemKey = keySelector(elem) const match = meta.find(el => keySelector(el) === elemKey) @@ -94,9 +91,9 @@ export default class MetadataDiff { private compareDeleted() { return ( - meta: XmlElement[], + meta: XmlContent[], keySelector: KeySelectorFn, - elem: XmlElement + elem: XmlContent ) => { const elemKey = keySelector(elem) return !meta.some(el => keySelector(el) === elemKey) @@ -129,10 +126,10 @@ class MetadataExtractor { return this.attributes.get(subType)?.key } - extractForSubType(fileContent: XmlContent, subType: string): XmlElement[] { + extractForSubType(fileContent: XmlContent, subType: string): XmlContent[] { const root = this.extractRootElement(fileContent) const content = root[subType] - return content ? asArray(content) : [] + return content ? castArray(content) : [] } isContentEmpty(fileContent: XmlContent): boolean { @@ -144,11 +141,11 @@ class MetadataExtractor { ) } - extractRootElement(fileContent: XmlContent): XmlElement { + extractRootElement(fileContent: XmlContent): XmlContent { const rootKey = Object.keys(fileContent).find(key => key !== XML_HEADER_ATTRIBUTE_KEY) ?? '' - return (fileContent[rootKey] as XmlElement) ?? {} + return (fileContent[rootKey] as XmlContent) ?? {} } } @@ -159,9 +156,9 @@ class MetadataComparator { baseContent: XmlContent, targetContent: XmlContent, elementMatcher: ( - meta: XmlElement[], + meta: XmlContent[], keySelector: KeySelectorFn, - elem: XmlElement + elem: XmlContent ) => boolean ): Manifest { return this.extractor @@ -217,16 +214,16 @@ class JsonTransformer { } private getPartialContentWithoutKey( - fromMeta: XmlElement[], - toMeta: XmlElement[] - ): XmlElement[] { + fromMeta: XmlContent[], + toMeta: XmlContent[] + ): XmlContent[] { return isEqual(fromMeta, toMeta) ? [] : toMeta } private getPartialContentWithKey( - fromMeta: XmlElement[], - toMeta: XmlElement[] - ): XmlElement[] { + fromMeta: XmlContent[], + toMeta: XmlContent[] + ): XmlContent[] { return differenceWith(toMeta, fromMeta, isEqual) } } From f35b52dd052d74b0e5c18f2e8beb88f24ad7f2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Colladon?= Date: Fri, 6 Dec 2024 09:19:45 +0100 Subject: [PATCH 7/7] refactor: further segregation of responsibility --- src/utils/metadataDiff.ts | 74 +++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/src/utils/metadataDiff.ts b/src/utils/metadataDiff.ts index ad935333..508fb3c4 100644 --- a/src/utils/metadataDiff.ts +++ b/src/utils/metadataDiff.ts @@ -48,19 +48,15 @@ export default class MetadataDiff { this.toContent = toContent this.fromContent = fromContent - const comparator = new MetadataComparator(this.extractor) - - const added = comparator.compare( - this.toContent, - this.fromContent, - this.compareAdded() - ) - const deleted = comparator.compare( + const comparator = new MetadataComparator( + this.extractor, this.fromContent, - this.toContent, - this.compareDeleted() + this.toContent ) + const added = comparator.getChanges() + const deleted = comparator.getDeletion() + return { added, deleted } } @@ -76,29 +72,6 @@ export default class MetadataDiff { isEmpty: this.extractor.isContentEmpty(prunedContent), } } - - private compareAdded() { - return ( - meta: XmlContent[], - keySelector: KeySelectorFn, - elem: XmlContent - ) => { - const elemKey = keySelector(elem) - const match = meta.find(el => keySelector(el) === elemKey) - return !match || !isEqual(match, elem) - } - } - - private compareDeleted() { - return ( - meta: XmlContent[], - keySelector: KeySelectorFn, - elem: XmlContent - ) => { - const elemKey = keySelector(elem) - return !meta.some(el => keySelector(el) === elemKey) - } - } } class MetadataExtractor { @@ -150,9 +123,21 @@ class MetadataExtractor { } class MetadataComparator { - constructor(private extractor: MetadataExtractor) {} + constructor( + private extractor: MetadataExtractor, + private fromContent: XmlContent, + private toContent: XmlContent + ) {} + + getChanges() { + return this.compare(this.toContent, this.fromContent, this.compareAdded) + } - compare( + getDeletion() { + return this.compare(this.fromContent, this.toContent, this.compareDeleted) + } + + private compare( baseContent: XmlContent, targetContent: XmlContent, elementMatcher: ( @@ -186,6 +171,25 @@ class MetadataComparator { return manifest }, new Map()) } + + 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) + } + + private compareDeleted = ( + meta: XmlContent[], + keySelector: KeySelectorFn, + elem: XmlContent + ) => { + const elemKey = keySelector(elem) + return !meta.some(el => keySelector(el) === elemKey) + } } class JsonTransformer {