@salesforce/lds-ads-bridge 1.432.0 → 1.434.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -483,7 +483,7 @@ const callbacks$1 = [];
483
483
  function register(r) {
484
484
  callbacks$1.forEach((callback) => callback(r));
485
485
  }
486
- // version: 1.432.0-a57c62b660
486
+ // version: 1.434.0-d8deb0d4ea
487
487
 
488
488
  /**
489
489
  * Returns true if the value acts like a Promise, i.e. has a "then" function,
@@ -4459,28 +4459,23 @@ let configurableCreateContentVersion = new Configurable();
4459
4459
  * Depth to which tracked fields will be added to a request that results from a cache miss.
4460
4460
  * A value of 0 inhibits the addition of tracked fields, 1 will add tracked fields that can
4461
4461
  * be reached by following 1 relationship from the root record, etc.
4462
- * @defaultValue '5', replicates the current behavior
4462
+ * @defaultValue '1'
4463
4463
  */
4464
- let trackedFieldDepthOnCacheMiss = 5;
4464
+ let trackedFieldDepthOnCacheMiss = 1;
4465
4465
  /**
4466
4466
  * Depth to which tracked fields will be added to a request that results from a merge conflict
4467
4467
  * A value of 0 inhibits the addition of tracked fields, 1 will add tracked fields that can
4468
4468
  * be reached by following 1 relationship from the root record, etc.
4469
- * @defaultValue '5', replicates the current behavior
4469
+ * @defaultValue '1'
4470
4470
  */
4471
- let trackedFieldDepthOnCacheMergeConflict = 5;
4471
+ let trackedFieldDepthOnCacheMergeConflict = 1;
4472
4472
  /**
4473
4473
  * Depth to which tracked fields will be added to a request that results from a notify change invocation by the consumer
4474
4474
  * A value of 0 inhibits the addition of tracked fields, 1 will add tracked fields that can
4475
4475
  * be reached by following 1 relationship from the root record, etc.
4476
- * @defaultValue '5', replicates the current behavior
4476
+ * @defaultValue '1'
4477
4477
  */
4478
- let trackedFieldDepthOnNotifyChange = 5;
4479
- /**
4480
- * Determines if we will only fetch the 'Id' field for the leaf relationship record
4481
- * @defaultValue 'false', replicates the current behavior and fetches all fields in the store for the leaf relationship record
4482
- */
4483
- let trackedFieldLeafNodeIdAndNameOnly = false;
4478
+ let trackedFieldDepthOnNotifyChange = 1;
4484
4479
  /**
4485
4480
  * One store enabled Get Object Info adapter
4486
4481
  */
@@ -4577,12 +4572,6 @@ const configurationForRestAdapters = {
4577
4572
  getTrackedFieldDepthOnNotifyChange: function () {
4578
4573
  return trackedFieldDepthOnNotifyChange;
4579
4574
  },
4580
- setTrackedFieldLeafNodeIdAndNameOnly: function (trackedFieldLeafNodeIdAndNameOnlyParam) {
4581
- trackedFieldLeafNodeIdAndNameOnly = trackedFieldLeafNodeIdAndNameOnlyParam;
4582
- },
4583
- getTrackedFieldLeafNodeIdAndNameOnly: function () {
4584
- return trackedFieldLeafNodeIdAndNameOnly;
4585
- },
4586
4575
  setRecordRepresentationIngestionOverride: function (ingest) {
4587
4576
  recordRepresentationIngestionOverride = ingest;
4588
4577
  },
@@ -6171,6 +6160,9 @@ function buildSelectionFromFields(fields, optionalFields = []) {
6171
6160
  return createRecordSelection(convertRecordFieldsArrayToTrie(fields, optionalFields));
6172
6161
  }
6173
6162
 
6163
+ // Unused suffixes, but leaving in as comments to avoid eslint errors
6164
+ // const CUSTOM_API_NAME_SUFFIX = '__c';
6165
+ // const DMO_API_NAME_SUFFIX = '__dlm';
6174
6166
  const CUSTOM_EXTERNAL_OBJECT_FIELD_SUFFIX = '__x';
6175
6167
  const RECORD_REPRESENTATION_ERROR_VERSION = 'RECORD_REPRESENTATION_ERROR_VERSION_1';
6176
6168
  const RECORD_REPRESENTATION_ERROR_STORE_METADATA_PARAMS = {
@@ -6197,7 +6189,7 @@ function addScalarField(current, leafNodeFieldKey) {
6197
6189
  }
6198
6190
  }
6199
6191
  function extractTrackedFieldsToTrie(recordId, node, root, config, visitedRecordIds = {}, depth = 0) {
6200
- var _a, _b;
6192
+ var _a;
6201
6193
  // Filter Error and null nodes
6202
6194
  if (!isGraphNode$1(node)) {
6203
6195
  return;
@@ -6235,13 +6227,11 @@ function extractTrackedFieldsToTrie(recordId, node, root, config, visitedRecordI
6235
6227
  if (!isGraphNode$1(field)) {
6236
6228
  continue;
6237
6229
  }
6238
- const { maxDepth, onlyFetchLeafNodeIdAndName } = config;
6230
+ const { maxDepth } = config;
6239
6231
  if (field.isScalar('value') === false && !Array.isArray((_a = field.data) === null || _a === void 0 ? void 0 : _a.value)) {
6240
6232
  if (depth + 1 > maxDepth) {
6241
- if (onlyFetchLeafNodeIdAndName === true) {
6242
- addScalarFieldId(current);
6243
- addScalarFieldName(current);
6244
- }
6233
+ addScalarFieldId(current);
6234
+ addScalarFieldName(current);
6245
6235
  continue;
6246
6236
  }
6247
6237
  const spanningLink = field.link('value');
@@ -6263,19 +6253,10 @@ function extractTrackedFieldsToTrie(recordId, node, root, config, visitedRecordI
6263
6253
  current.children[key] = next;
6264
6254
  }
6265
6255
  else {
6266
- // Skip the field, if its value is null at the max level depth.
6267
- // Ideally, it should only skip relationship field. However,
6268
- // on the client, there is not a reliable way to determine the
6269
- // the field type.
6270
6256
  if (depth === maxDepth) {
6271
- if (onlyFetchLeafNodeIdAndName === true) {
6272
- addScalarFieldId(current);
6273
- addScalarFieldName(current);
6274
- continue;
6275
- }
6276
- if (field.scalar('value') === null && !Array.isArray((_b = field.data) === null || _b === void 0 ? void 0 : _b.value)) {
6277
- continue;
6278
- }
6257
+ addScalarFieldId(current);
6258
+ addScalarFieldName(current);
6259
+ continue;
6279
6260
  }
6280
6261
  const state = fieldValueRep.linkData();
6281
6262
  if (state !== undefined) {
@@ -6610,7 +6591,6 @@ function mergeRecordConflict(luvio, incoming, existing, recordConflictMap) {
6610
6591
  };
6611
6592
  const trackedFieldsConfig = {
6612
6593
  maxDepth: configurationForRestAdapters.getTrackedFieldDepthOnCacheMergeConflict(),
6613
- onlyFetchLeafNodeIdAndName: configurationForRestAdapters.getTrackedFieldLeafNodeIdAndNameOnly(),
6614
6594
  };
6615
6595
  extractTrackedFieldsToTrie(recordKey, incomingNode, incomingTrackedFieldsTrieRoot, trackedFieldsConfig);
6616
6596
  extractTrackedFieldsToTrie(recordKey, existingNode, existingTrackedFieldsTrieRoot, trackedFieldsConfig);
@@ -7048,7 +7028,6 @@ function prepareRequest$7(luvio, config) {
7048
7028
  const key = keyBuilder$3T(luvio, createResourceParams$15(config));
7049
7029
  const allTrackedFields = getTrackedFields(key, luvio.getNode(key), {
7050
7030
  maxDepth: configurationForRestAdapters.getTrackedFieldDepthOnCacheMiss(),
7051
- onlyFetchLeafNodeIdAndName: configurationForRestAdapters.getTrackedFieldLeafNodeIdAndNameOnly(),
7052
7031
  }, config.optionalFields);
7053
7032
  const optionalFields = fields === undefined ? allTrackedFields : difference(allTrackedFields, fields);
7054
7033
  const resourceParams = createResourceParams$15({
@@ -7141,7 +7120,6 @@ function ingestSuccessChildResourceParams$9(luvio, childResourceParamsArray, chi
7141
7120
  };
7142
7121
  const childTrackedFields = getTrackedFields(childKey, luvio.getNode(childKey), {
7143
7122
  maxDepth: configurationForRestAdapters.getTrackedFieldDepthOnCacheMiss(),
7144
- onlyFetchLeafNodeIdAndName: configurationForRestAdapters.getTrackedFieldLeafNodeIdAndNameOnly(),
7145
7123
  }, childResourceParams.queryParams.optionalFields);
7146
7124
  const childSnapshot = ingestSuccess$X(luvio, {
7147
7125
  recordId: childResourceParams.urlParams.recordId,
@@ -7900,6 +7878,9 @@ function isSpanningRecord(fieldValue) {
7900
7878
  function isStoreKeyRecordId(key) {
7901
7879
  return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
7902
7880
  }
7881
+ function isStoreKeyRecordField(key) {
7882
+ return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) > -1;
7883
+ }
7903
7884
  /**
7904
7885
  * Returns a shallow copy of a record with its field values if it is a scalar and a reference and a
7905
7886
  * a RecordRepresentation with no field if the value if a spanning record.
@@ -8194,6 +8175,33 @@ class AdsBridge {
8194
8175
  this.isRecordEmitLocked = false;
8195
8176
  }
8196
8177
  }
8178
+ filterAndSynthesizeBaseIds(updatedEntries) {
8179
+ // Collect base record IDs that are directly present in the entries.
8180
+ const directBaseIds = new Set();
8181
+ for (let i = 0; i < updatedEntries.length; i++) {
8182
+ if (isStoreKeyRecordId(updatedEntries[i].id)) {
8183
+ directBaseIds.add(updatedEntries[i].id);
8184
+ }
8185
+ }
8186
+ // For field entries whose base record ID has no direct entry, synthesize one.
8187
+ const syntheticBaseIds = new Set();
8188
+ for (let i = 0; i < updatedEntries.length; i++) {
8189
+ if (isStoreKeyRecordField(updatedEntries[i].id)) {
8190
+ const baseId = updatedEntries[i].id.split(RECORD_FIELDS_KEY_JUNCTION)[0];
8191
+ if (!directBaseIds.has(baseId)) {
8192
+ syntheticBaseIds.add(baseId);
8193
+ }
8194
+ }
8195
+ }
8196
+ // Exclude all the store record ids not matching with the record id pattern.
8197
+ // Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
8198
+ // need to filter them out.
8199
+ const filteredUpdatedEntries = updatedEntries.filter((entry) => isStoreKeyRecordId(entry.id));
8200
+ syntheticBaseIds.forEach((baseId) => {
8201
+ push.call(filteredUpdatedEntries, { id: baseId });
8202
+ });
8203
+ return filteredUpdatedEntries;
8204
+ }
8197
8205
  /**
8198
8206
  * This method retrieves queries the store with with passed record ids to retrieve their
8199
8207
  * associated records and object info. Note that the passed ids are not Salesforce record id
@@ -8204,14 +8212,10 @@ class AdsBridge {
8204
8212
  let shouldEmit = false;
8205
8213
  const adsRecordMap = {};
8206
8214
  const adsObjectMap = {};
8207
- for (let i = 0; i < updatedEntries.length; i++) {
8208
- const storeRecordId = updatedEntries[i].id;
8209
- // Exclude all the store record ids not matching with the record id pattern.
8210
- // Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
8211
- // need to filter them out.
8212
- if (!isStoreKeyRecordId(storeRecordId)) {
8213
- continue;
8214
- }
8215
+ // Context for change: W-21715343
8216
+ const filteredUpdatedEntries = this.filterAndSynthesizeBaseIds(updatedEntries);
8217
+ for (let i = 0; i < filteredUpdatedEntries.length; i++) {
8218
+ const storeRecordId = filteredUpdatedEntries[i].id;
8215
8219
  const record = this.recordRepresentationIngestOverride !== undefined
8216
8220
  ? getShallowRecordDenormalized(luvio, storeRecordId)
8217
8221
  : getShallowRecord(luvio, storeRecordId);
package/dist/adsBridge.js CHANGED
@@ -58,6 +58,9 @@ function isSpanningRecord(fieldValue) {
58
58
  function isStoreKeyRecordId(key) {
59
59
  return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
60
60
  }
61
+ function isStoreKeyRecordField(key) {
62
+ return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) > -1;
63
+ }
61
64
  /**
62
65
  * Returns a shallow copy of a record with its field values if it is a scalar and a reference and a
63
66
  * a RecordRepresentation with no field if the value if a spanning record.
@@ -356,6 +359,33 @@ class AdsBridge {
356
359
  this.isRecordEmitLocked = false;
357
360
  }
358
361
  }
362
+ filterAndSynthesizeBaseIds(updatedEntries) {
363
+ // Collect base record IDs that are directly present in the entries.
364
+ const directBaseIds = new Set();
365
+ for (let i = 0; i < updatedEntries.length; i++) {
366
+ if (isStoreKeyRecordId(updatedEntries[i].id)) {
367
+ directBaseIds.add(updatedEntries[i].id);
368
+ }
369
+ }
370
+ // For field entries whose base record ID has no direct entry, synthesize one.
371
+ const syntheticBaseIds = new Set();
372
+ for (let i = 0; i < updatedEntries.length; i++) {
373
+ if (isStoreKeyRecordField(updatedEntries[i].id)) {
374
+ const baseId = updatedEntries[i].id.split(RECORD_FIELDS_KEY_JUNCTION)[0];
375
+ if (!directBaseIds.has(baseId)) {
376
+ syntheticBaseIds.add(baseId);
377
+ }
378
+ }
379
+ }
380
+ // Exclude all the store record ids not matching with the record id pattern.
381
+ // Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
382
+ // need to filter them out.
383
+ const filteredUpdatedEntries = updatedEntries.filter((entry) => isStoreKeyRecordId(entry.id));
384
+ syntheticBaseIds.forEach((baseId) => {
385
+ push.call(filteredUpdatedEntries, { id: baseId });
386
+ });
387
+ return filteredUpdatedEntries;
388
+ }
359
389
  /**
360
390
  * This method retrieves queries the store with with passed record ids to retrieve their
361
391
  * associated records and object info. Note that the passed ids are not Salesforce record id
@@ -367,14 +397,10 @@ class AdsBridge {
367
397
  let shouldEmit = false;
368
398
  const adsRecordMap = {};
369
399
  const adsObjectMap = {};
370
- for (let i = 0; i < updatedEntries.length; i++) {
371
- const storeRecordId = updatedEntries[i].id;
372
- // Exclude all the store record ids not matching with the record id pattern.
373
- // Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
374
- // need to filter them out.
375
- if (!isStoreKeyRecordId(storeRecordId)) {
376
- continue;
377
- }
400
+ // Context for change: W-21715343
401
+ const filteredUpdatedEntries = this.filterAndSynthesizeBaseIds(updatedEntries);
402
+ for (let i = 0; i < filteredUpdatedEntries.length; i++) {
403
+ const storeRecordId = filteredUpdatedEntries[i].id;
378
404
  const record = this.recordRepresentationIngestOverride !== undefined
379
405
  ? getShallowRecordDenormalized(luvio, storeRecordId)
380
406
  : getShallowRecord(luvio, storeRecordId);
@@ -435,4 +461,4 @@ function withAdsBridge(callback) {
435
461
  }
436
462
 
437
463
  export { instrument, withAdsBridge };
438
- // version: 1.432.0-a57c62b660
464
+ // version: 1.434.0-d8deb0d4ea
@@ -78,6 +78,7 @@ export default class AdsBridge {
78
78
  * mutations triggered by ADS to be emit back to ADS.
79
79
  */
80
80
  private lockLdsRecordEmit;
81
+ private filterAndSynthesizeBaseIds;
81
82
  /**
82
83
  * This method retrieves queries the store with with passed record ids to retrieve their
83
84
  * associated records and object info. Note that the passed ids are not Salesforce record id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-ads-bridge",
3
- "version": "1.432.0",
3
+ "version": "1.434.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "Bridge to sync data between LDS and ADS",
6
6
  "main": "dist/adsBridge.js",
@@ -30,9 +30,9 @@
30
30
  "release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-ads-bridge"
31
31
  },
32
32
  "devDependencies": {
33
- "@salesforce/lds-adapters-uiapi": "^1.432.0",
34
- "@salesforce/lds-runtime-mobile": "^1.432.0",
35
- "@salesforce/lds-uiapi-record-utils-mobile": "^1.432.0"
33
+ "@salesforce/lds-adapters-uiapi": "^1.434.0",
34
+ "@salesforce/lds-runtime-mobile": "^1.434.0",
35
+ "@salesforce/lds-uiapi-record-utils-mobile": "^1.434.0"
36
36
  },
37
37
  "volta": {
38
38
  "extends": "../../package.json"
@@ -41,9 +41,9 @@
41
41
  {
42
42
  "path": "./dist/adsBridge.js",
43
43
  "maxSize": {
44
- "none": "17 kB",
45
- "min": "5.51 kB",
46
- "compressed": "4 kB"
44
+ "none": "18.4 kB",
45
+ "min": "6 kB",
46
+ "compressed": "4.3 kB"
47
47
  }
48
48
  }
49
49
  ]
@@ -1,5 +1,10 @@
1
1
  import { Luvio, InMemoryStore, Environment } from '@luvio/engine';
2
- import { keyBuilderRecord, ingestRecord, type Registration } from '@salesforce/lds-adapters-uiapi';
2
+ import {
3
+ keyBuilderRecord,
4
+ ingestRecord,
5
+ type Registration,
6
+ ingestRecordSuccess,
7
+ } from '@salesforce/lds-adapters-uiapi';
3
8
  import { expect } from '@jest/globals';
4
9
 
5
10
  import AdsBridge from '../ads-bridge';
@@ -417,6 +422,158 @@ describe('AdsBridge', () => {
417
422
  });
418
423
  });
419
424
 
425
+ // Context for change: W-21715343
426
+ it('correctly emits the updated Case record to ADS when refreshSnapshot is called, and the Case record was updated via spanning record updates on the Task record', async () => {
427
+ const initialLdsState = JSON.parse(JSON.stringify(require('./initial_record.json')));
428
+ const addRecordsGet = JSON.parse(JSON.stringify(require('./task_record_gvp_get.json')));
429
+ const addRecordsSave = JSON.parse(
430
+ JSON.stringify(require('./task_record_gvp_save.json'))
431
+ );
432
+ const addRecordsGet2 = JSON.parse(
433
+ JSON.stringify(require('./task_record_gvp_get2.json'))
434
+ );
435
+
436
+ const { bridge, luvio } = createBridge();
437
+ const caseId: string = initialLdsState.id;
438
+
439
+ const selector = {
440
+ recordId: keyBuilderRecord(luvio, { recordId: caseId }),
441
+ node: { kind: 'Fragment' as const, private: [] as string[] },
442
+ variables: {},
443
+ };
444
+
445
+ // Creating a refreshable snapshot of the Case record as we don't have the wire adapter bindings available to us in jest environment to refresh the snapshot manually.
446
+ const snapshot = luvio.storeLookup(selector, {
447
+ config: { recordId: caseId },
448
+ resolve: async () => {
449
+ const response = await luvio.dispatchResourceRequest({
450
+ baseUri: '',
451
+ basePath: `/ui-api/records/${caseId}`,
452
+ method: 'get',
453
+ body: null,
454
+ queryParams: {},
455
+ urlParams: {},
456
+ headers: {},
457
+ priority: 'normal',
458
+ });
459
+ const config = {
460
+ optionalFields: ['Case.Id'],
461
+ recordId: caseId,
462
+ };
463
+ const trackedFields = ['Case.Status'];
464
+ ingestRecordSuccess(luvio, config, caseId, trackedFields, response as any, 0);
465
+ await luvio.storeBroadcast();
466
+ return luvio.storeLookup(selector);
467
+ },
468
+ });
469
+
470
+ // 1. Prime LDS with the Case fixture (via addRecord → storeIngest + storeBroadcast).
471
+ addRecord(luvio, initialLdsState);
472
+
473
+ const expectedStatus = queryRecord(luvio, { recordId: caseId }).data.fields.Status;
474
+ expect(expectedStatus).toEqual({ value: 'New', displayValue: 'New' });
475
+
476
+ luvio.dispatchResourceRequest = jest.fn();
477
+
478
+ const fn = jest.fn();
479
+ bridge.receiveFromLdsCallback = fn;
480
+
481
+ // 2. Ingest the Task (+ spanning Case__r) via addRecords().
482
+ bridge.addRecords(addRecordsGet); // A RecordGvp.getRecord call is made to get the initial Task record when switching from the Case record to the Task record in console mode.
483
+ bridge.addRecords(addRecordsSave); // Clicking the complete button on the Task record which does a RecordGvp.saveRecord call.
484
+ bridge.addRecords(addRecordsGet2); // A RecordGvp.getRecord call is made to get the updated Task record.
485
+
486
+ expect(luvio.dispatchResourceRequest).toHaveBeenCalledTimes(0);
487
+
488
+ // 3. Synthesize the closed Case record server response which is expected to be received when the refreshSnapshot is called.
489
+ const closedCaseResponse = JSON.parse(JSON.stringify(require('./initial_record.json')));
490
+ closedCaseResponse.fields.Status = { value: 'Closed', displayValue: 'Closed' };
491
+ closedCaseResponse.weakEtag = addRecordsGet2[0].weakEtag;
492
+ closedCaseResponse.systemModstamp = addRecordsGet2[0].systemModstamp;
493
+ closedCaseResponse.lastModifiedDate = addRecordsGet2[0].lastModifiedDate;
494
+
495
+ luvio.dispatchResourceRequest = jest
496
+ .fn()
497
+ .mockResolvedValueOnce({ body: closedCaseResponse })
498
+ .mockImplementation(() => new Promise(() => {}));
499
+
500
+ // 4. Refresh the snapshot to get the updated Case record. This was previously not working because the interactions on the ADS-Bridge had updated the Case record with the latest weakEtag, but NOT the updated Status field value.
501
+ const refreshedSnapshot = await luvio.refreshSnapshot(snapshot);
502
+ expect((refreshedSnapshot.data as any).fields.Status).toEqual(
503
+ expect.objectContaining({ value: 'Closed' })
504
+ );
505
+
506
+ // 5. With the ADS-Bridge changes, the Case record is now emitted when the refreshSnapshot is called.
507
+ expect(fn.mock.calls[0]).toMatchInlineSnapshot(`
508
+ Array [
509
+ Object {
510
+ "500xx000000boDJAAY": Object {
511
+ "Case": Object {
512
+ "isPrimary": true,
513
+ "record": Object {
514
+ "apiName": "Case",
515
+ "childRelationships": Object {},
516
+ "eTag": "",
517
+ "fields": Object {
518
+ "CaseNumber": Object {
519
+ "displayValue": null,
520
+ "value": "00001026",
521
+ },
522
+ "Id": Object {
523
+ "displayValue": null,
524
+ "value": "500xx000000boDJAAY",
525
+ },
526
+ "IsEscalated": Object {
527
+ "displayValue": null,
528
+ "value": false,
529
+ },
530
+ "MasterRecordId": Object {
531
+ "displayValue": null,
532
+ "value": null,
533
+ },
534
+ "Priority": Object {
535
+ "displayValue": "Medium",
536
+ "value": "Medium",
537
+ },
538
+ "RecordTypeId": Object {
539
+ "displayValue": null,
540
+ "value": "012000000000000AAA",
541
+ },
542
+ "Status": Object {
543
+ "displayValue": "Closed",
544
+ "value": "Closed",
545
+ },
546
+ "Subject": Object {
547
+ "displayValue": null,
548
+ "value": "Fixture subject",
549
+ },
550
+ "SystemModstamp": Object {
551
+ "displayValue": null,
552
+ "value": "2026-04-11T00:46:56.000Z",
553
+ },
554
+ },
555
+ "id": "500xx000000boDJAAY",
556
+ "lastModifiedById": "005xx000001X8gHAAS",
557
+ "lastModifiedDate": "2026-04-13T20:54:41.000Z",
558
+ "recordTypeId": "012000000000000AAA",
559
+ "recordTypeInfo": null,
560
+ "systemModstamp": "2026-04-13T20:54:41.000Z",
561
+ "weakEtag": 1776113681000,
562
+ },
563
+ },
564
+ },
565
+ },
566
+ Object {
567
+ "Case": Object {
568
+ "_entityLabel": "Case",
569
+ "_keyPrefix": "500",
570
+ "_nameField": "Name",
571
+ },
572
+ },
573
+ ]
574
+ `);
575
+ });
576
+
420
577
  describe('displayValue', () => {
421
578
  it('does not let null overwrite non-null displayValue', () => {
422
579
  const { bridge, luvio } = createBridge();
@@ -1383,6 +1540,82 @@ describe('AdsBridge', () => {
1383
1540
  });
1384
1541
  });
1385
1542
 
1543
+ describe('filterAndSynthesizeBaseIds', () => {
1544
+ const BASE_KEY = 'UiApi::RecordRepresentation:001xx000000001AAA';
1545
+ const FIELD_KEY = 'UiApi::RecordRepresentation:001xx000000001AAA__fields__Name';
1546
+ const BASE_KEY_2 = 'UiApi::RecordRepresentation:001xx000000002AAA';
1547
+ const FIELD_KEY_2A = 'UiApi::RecordRepresentation:001xx000000002AAA__fields__Name';
1548
+ const FIELD_KEY_2B = 'UiApi::RecordRepresentation:001xx000000002AAA__fields__Id';
1549
+ const NON_RECORD_KEY = 'SomeOther::Thing:abc123';
1550
+
1551
+ function callFilter(bridge: AdsBridge, entries: { id: string }[]): { id: string }[] {
1552
+ return (bridge as any).filterAndSynthesizeBaseIds(entries);
1553
+ }
1554
+
1555
+ it('returns an empty array when given an empty array', () => {
1556
+ const { bridge } = createBridge();
1557
+ expect(callFilter(bridge, [])).toEqual([]);
1558
+ });
1559
+
1560
+ it('filters out non-record entries', () => {
1561
+ const { bridge } = createBridge();
1562
+ const result = callFilter(bridge, [{ id: NON_RECORD_KEY }]);
1563
+ expect(result).toEqual([]);
1564
+ });
1565
+
1566
+ it('returns base record entries unchanged', () => {
1567
+ const { bridge } = createBridge();
1568
+ const result = callFilter(bridge, [{ id: BASE_KEY }]);
1569
+ expect(result).toEqual([{ id: BASE_KEY }]);
1570
+ });
1571
+
1572
+ it('synthesizes a base record entry for a field entry with no direct base entry', () => {
1573
+ const { bridge } = createBridge();
1574
+ const result = callFilter(bridge, [{ id: FIELD_KEY }]);
1575
+ expect(result).toEqual([{ id: BASE_KEY }]);
1576
+ });
1577
+
1578
+ it('does not synthesize a duplicate base entry when a direct base entry is already present', () => {
1579
+ const { bridge } = createBridge();
1580
+ const result = callFilter(bridge, [{ id: BASE_KEY }, { id: FIELD_KEY }]);
1581
+ expect(result).toHaveLength(1);
1582
+ expect(result).toEqual([{ id: BASE_KEY }]);
1583
+ });
1584
+
1585
+ it('synthesizes exactly one base entry for multiple field entries sharing the same base', () => {
1586
+ const { bridge } = createBridge();
1587
+ const result = callFilter(bridge, [{ id: FIELD_KEY_2A }, { id: FIELD_KEY_2B }]);
1588
+ expect(result).toHaveLength(1);
1589
+ expect(result).toEqual([{ id: BASE_KEY_2 }]);
1590
+ });
1591
+
1592
+ it('returns base entries and synthesizes entries for fields whose base is not directly present', () => {
1593
+ const { bridge } = createBridge();
1594
+ const entries = [
1595
+ { id: BASE_KEY },
1596
+ { id: FIELD_KEY },
1597
+ { id: FIELD_KEY_2A },
1598
+ { id: FIELD_KEY_2B },
1599
+ ];
1600
+ const result = callFilter(bridge, entries);
1601
+ expect(result).toHaveLength(2);
1602
+ expect(result).toContainEqual({ id: BASE_KEY });
1603
+ expect(result).toContainEqual({ id: BASE_KEY_2 });
1604
+ });
1605
+
1606
+ it('filters out non-record entries while still processing valid entries', () => {
1607
+ const { bridge } = createBridge();
1608
+ const result = callFilter(bridge, [
1609
+ { id: NON_RECORD_KEY },
1610
+ { id: BASE_KEY },
1611
+ { id: FIELD_KEY_2A },
1612
+ ]);
1613
+ expect(result).toHaveLength(2);
1614
+ expect(result).toContainEqual({ id: BASE_KEY });
1615
+ expect(result).toContainEqual({ id: BASE_KEY_2 });
1616
+ });
1617
+ });
1618
+
1386
1619
  describe('isDMOEntity', () => {
1387
1620
  it('should return true for DMO record', () => {
1388
1621
  const record = createRecord({
@@ -0,0 +1,50 @@
1
+ {
2
+ "apiName": "Case",
3
+ "childRelationships": {},
4
+ "eTag": "",
5
+ "fields": {
6
+ "Id": {
7
+ "displayValue": null,
8
+ "value": "500xx000000boDJAAY"
9
+ },
10
+ "RecordTypeId": {
11
+ "displayValue": null,
12
+ "value": "012000000000000AAA"
13
+ },
14
+ "IsEscalated": {
15
+ "displayValue": null,
16
+ "value": false
17
+ },
18
+ "MasterRecordId": {
19
+ "displayValue": null,
20
+ "value": null
21
+ },
22
+ "SystemModstamp": {
23
+ "displayValue": null,
24
+ "value": "2026-04-11T00:46:56.000Z"
25
+ },
26
+ "CaseNumber": {
27
+ "displayValue": null,
28
+ "value": "00001026"
29
+ },
30
+ "Priority": {
31
+ "displayValue": "Medium",
32
+ "value": "Medium"
33
+ },
34
+ "Status": {
35
+ "displayValue": "New",
36
+ "value": "New"
37
+ },
38
+ "Subject": {
39
+ "displayValue": null,
40
+ "value": "Fixture subject"
41
+ }
42
+ },
43
+ "id": "500xx000000boDJAAY",
44
+ "lastModifiedById": "005xx000001X8gHAAS",
45
+ "lastModifiedDate": "2026-04-13T20:50:18.000Z",
46
+ "recordTypeId": "012000000000000AAA",
47
+ "recordTypeInfo": null,
48
+ "systemModstamp": "2026-04-13T20:50:18.000Z",
49
+ "weakEtag": 1776113418000
50
+ }