Skip to content

Commit 185464e

Browse files
committed
fix: W-23149249 CI/CD CLI plugin - Objects without data records should be included in import API payload
1 parent e6b6271 commit 185464e

2 files changed

Lines changed: 102 additions & 15 deletions

File tree

src/commands/data/setup/transfer.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -176,37 +176,45 @@ export default class SetupTransfer extends SfCommand<SetupTransferResult> {
176176
definition: DefinitionFile,
177177
data: { metadata: Record<string, unknown>; objects: ExportEntity[] }
178178
): MergedPayload {
179-
const defEntityMap = new Map<string, DefinitionEntity>();
180-
for (const entity of definition.objects.list) {
181-
defEntityMap.set(entity.objectName, entity);
179+
const dataEntityMap = new Map<string, ExportEntity>();
180+
for (const entity of data.objects ?? []) {
181+
dataEntityMap.set(entity.objectName, entity);
182182
}
183183

184184
const importSequence = definition.importSequence?.list ?? [];
185185
const metadata = data.metadata ?? {};
186186

187-
const mergedEntities: MergedEntity[] = data.objects.map((dataEntity) => {
188-
const defEntity = defEntityMap.get(dataEntity.objectName);
187+
// Drive the merge from the definition so that every object declared in the
188+
// DefinitionFile is included in the import payload, even when the export
189+
// returned no data record for it (records default to an empty array).
190+
const mergedEntities: MergedEntity[] = definition.objects.list.map((defEntity) => {
191+
const dataEntity = dataEntityMap.get(defEntity.objectName);
189192

190193
const header: MergedEntityHeader = {
191-
objectName: dataEntity.objectName,
192-
fields: defEntity?.fields ?? '',
193-
filterCriteria: defEntity?.filterCriteria ?? '',
194-
foreignKeys: defEntity?.foreignKeys?.list ?? [],
194+
objectName: defEntity.objectName,
195+
fields: defEntity.fields ?? '',
196+
filterCriteria: defEntity.filterCriteria ?? '',
197+
foreignKeys: defEntity.foreignKeys?.list ?? [],
195198
};
196-
if (defEntity?.globalKeyField) {
199+
if (defEntity.globalKeyField) {
197200
header.globalKeyField = defEntity.globalKeyField;
198201
}
199-
if (defEntity?.compositeKeys?.list) {
202+
if (defEntity.compositeKeys?.list) {
200203
header.compositeKeys = defEntity.compositeKeys.list;
201204
}
202205

203206
const merged: MergedEntity = {
204207
header,
205-
objectName: dataEntity.objectName,
206-
records: dataEntity.records,
208+
objectName: defEntity.objectName,
209+
records: dataEntity?.records ?? [],
207210
};
208-
if (dataEntity.recordCount != null) {
209-
merged.recordCount = dataEntity.recordCount;
211+
if (dataEntity) {
212+
if (dataEntity.recordCount != null) {
213+
merged.recordCount = dataEntity.recordCount;
214+
}
215+
} else {
216+
// The export returned nothing for this object; record an explicit zero count.
217+
merged.recordCount = 0;
210218
}
211219
return merged;
212220
});

test/commands/data/setup/transfer.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,85 @@ describe('data setup transfer', () => {
165165
});
166166
});
167167

168+
describe('custom definition mode', () => {
169+
it('includes every definition object in the import payload even without a data record', async () => {
170+
// Definition declares two objects; export only returns data for Account.
171+
const definition = {
172+
dataSetName: 'customDefinition',
173+
version: '1.0.0',
174+
importSequence: { list: ['Account', 'Contact'] },
175+
objects: {
176+
list: [
177+
{
178+
objectName: 'Account',
179+
globalKeyField: 'Name',
180+
fields: 'Id, Name',
181+
filterCriteria: '',
182+
foreignKeys: { list: [] },
183+
},
184+
{
185+
objectName: 'Contact',
186+
globalKeyField: 'Email',
187+
fields: 'Id, Email',
188+
filterCriteria: '',
189+
foreignKeys: { list: [] },
190+
},
191+
],
192+
},
193+
};
194+
195+
const defFile = path.join(os.tmpdir(), `custom-definition-${Date.now()}.json`);
196+
fs.writeFileSync(defFile, JSON.stringify(definition), 'utf8');
197+
198+
let importPayload:
199+
| { objects: Array<{ objectName: string; records: unknown[]; recordCount?: number }> }
200+
| undefined;
201+
$$.fakeConnectionRequest = (request) => {
202+
if (typeof request === 'string' || (request as { url: string }).url.includes('/export')) {
203+
// Export returns data for Account only — Contact has no records.
204+
return Promise.resolve({
205+
isSuccess: true,
206+
metadata: { dataSetName: 'customDefinition', version: '1.0.0' },
207+
objects: [
208+
{
209+
objectName: 'Account',
210+
recordCount: 1,
211+
records: [{ Id: '001xx000000001', Name: 'Test Account 1' }],
212+
},
213+
],
214+
});
215+
}
216+
importPayload = JSON.parse((request as { body: string }).body) as typeof importPayload;
217+
return Promise.resolve(mockImportResponse);
218+
};
219+
220+
try {
221+
await SetupTransfer.run([
222+
'--extended-definition-file',
223+
defFile,
224+
'--source-org',
225+
'source@test.org',
226+
'--target-org',
227+
'target@test.org',
228+
]);
229+
} finally {
230+
fs.rmSync(defFile, { force: true });
231+
}
232+
233+
expect(importPayload).to.exist;
234+
const objectNames = importPayload!.objects.map((o) => o.objectName);
235+
expect(objectNames).to.deep.equal(['Account', 'Contact']);
236+
237+
const contact = importPayload!.objects.find((o) => o.objectName === 'Contact');
238+
expect(contact!.records).to.deep.equal([]);
239+
expect(contact!.recordCount).to.equal(0);
240+
241+
const account = importPayload!.objects.find((o) => o.objectName === 'Account');
242+
expect(account!.records).to.have.lengthOf(1);
243+
expect(account!.recordCount).to.equal(1);
244+
});
245+
});
246+
168247
describe('error handling', () => {
169248
it('throws error when export API returns error', async () => {
170249
$$.fakeConnectionRequest = (request) => {

0 commit comments

Comments
 (0)