From 185464e1234ebf4bae051522352b763f8545b395 Mon Sep 17 00:00:00 2001 From: John Chen Date: Tue, 23 Jun 2026 18:48:16 -0700 Subject: [PATCH] fix: W-23149249 CI/CD CLI plugin - Objects without data records should be included in import API payload --- src/commands/data/setup/transfer.ts | 38 ++++++----- test/commands/data/setup/transfer.test.ts | 79 +++++++++++++++++++++++ 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/src/commands/data/setup/transfer.ts b/src/commands/data/setup/transfer.ts index 40be2e5..764313c 100644 --- a/src/commands/data/setup/transfer.ts +++ b/src/commands/data/setup/transfer.ts @@ -176,37 +176,45 @@ export default class SetupTransfer extends SfCommand { definition: DefinitionFile, data: { metadata: Record; objects: ExportEntity[] } ): MergedPayload { - const defEntityMap = new Map(); - for (const entity of definition.objects.list) { - defEntityMap.set(entity.objectName, entity); + const dataEntityMap = new Map(); + for (const entity of data.objects ?? []) { + dataEntityMap.set(entity.objectName, entity); } const importSequence = definition.importSequence?.list ?? []; const metadata = data.metadata ?? {}; - const mergedEntities: MergedEntity[] = data.objects.map((dataEntity) => { - const defEntity = defEntityMap.get(dataEntity.objectName); + // Drive the merge from the definition so that every object declared in the + // DefinitionFile is included in the import payload, even when the export + // returned no data record for it (records default to an empty array). + const mergedEntities: MergedEntity[] = definition.objects.list.map((defEntity) => { + const dataEntity = dataEntityMap.get(defEntity.objectName); const header: MergedEntityHeader = { - objectName: dataEntity.objectName, - fields: defEntity?.fields ?? '', - filterCriteria: defEntity?.filterCriteria ?? '', - foreignKeys: defEntity?.foreignKeys?.list ?? [], + objectName: defEntity.objectName, + fields: defEntity.fields ?? '', + filterCriteria: defEntity.filterCriteria ?? '', + foreignKeys: defEntity.foreignKeys?.list ?? [], }; - if (defEntity?.globalKeyField) { + if (defEntity.globalKeyField) { header.globalKeyField = defEntity.globalKeyField; } - if (defEntity?.compositeKeys?.list) { + if (defEntity.compositeKeys?.list) { header.compositeKeys = defEntity.compositeKeys.list; } const merged: MergedEntity = { header, - objectName: dataEntity.objectName, - records: dataEntity.records, + objectName: defEntity.objectName, + records: dataEntity?.records ?? [], }; - if (dataEntity.recordCount != null) { - merged.recordCount = dataEntity.recordCount; + if (dataEntity) { + if (dataEntity.recordCount != null) { + merged.recordCount = dataEntity.recordCount; + } + } else { + // The export returned nothing for this object; record an explicit zero count. + merged.recordCount = 0; } return merged; }); diff --git a/test/commands/data/setup/transfer.test.ts b/test/commands/data/setup/transfer.test.ts index 65fd80c..186917e 100644 --- a/test/commands/data/setup/transfer.test.ts +++ b/test/commands/data/setup/transfer.test.ts @@ -165,6 +165,85 @@ describe('data setup transfer', () => { }); }); + describe('custom definition mode', () => { + it('includes every definition object in the import payload even without a data record', async () => { + // Definition declares two objects; export only returns data for Account. + const definition = { + dataSetName: 'customDefinition', + version: '1.0.0', + importSequence: { list: ['Account', 'Contact'] }, + objects: { + list: [ + { + objectName: 'Account', + globalKeyField: 'Name', + fields: 'Id, Name', + filterCriteria: '', + foreignKeys: { list: [] }, + }, + { + objectName: 'Contact', + globalKeyField: 'Email', + fields: 'Id, Email', + filterCriteria: '', + foreignKeys: { list: [] }, + }, + ], + }, + }; + + const defFile = path.join(os.tmpdir(), `custom-definition-${Date.now()}.json`); + fs.writeFileSync(defFile, JSON.stringify(definition), 'utf8'); + + let importPayload: + | { objects: Array<{ objectName: string; records: unknown[]; recordCount?: number }> } + | undefined; + $$.fakeConnectionRequest = (request) => { + if (typeof request === 'string' || (request as { url: string }).url.includes('/export')) { + // Export returns data for Account only — Contact has no records. + return Promise.resolve({ + isSuccess: true, + metadata: { dataSetName: 'customDefinition', version: '1.0.0' }, + objects: [ + { + objectName: 'Account', + recordCount: 1, + records: [{ Id: '001xx000000001', Name: 'Test Account 1' }], + }, + ], + }); + } + importPayload = JSON.parse((request as { body: string }).body) as typeof importPayload; + return Promise.resolve(mockImportResponse); + }; + + try { + await SetupTransfer.run([ + '--extended-definition-file', + defFile, + '--source-org', + 'source@test.org', + '--target-org', + 'target@test.org', + ]); + } finally { + fs.rmSync(defFile, { force: true }); + } + + expect(importPayload).to.exist; + const objectNames = importPayload!.objects.map((o) => o.objectName); + expect(objectNames).to.deep.equal(['Account', 'Contact']); + + const contact = importPayload!.objects.find((o) => o.objectName === 'Contact'); + expect(contact!.records).to.deep.equal([]); + expect(contact!.recordCount).to.equal(0); + + const account = importPayload!.objects.find((o) => o.objectName === 'Account'); + expect(account!.records).to.have.lengthOf(1); + expect(account!.recordCount).to.equal(1); + }); + }); + describe('error handling', () => { it('throws error when export API returns error', async () => { $$.fakeConnectionRequest = (request) => {