@salesforce/lds-ads-bridge 1.339.0 → 1.341.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.
@@ -0,0 +1,1378 @@
1
+ import { Luvio, InMemoryStore, Environment } from '@luvio/engine';
2
+ import {
3
+ keyBuilderRecord,
4
+ getRecordIngestionOverride,
5
+ type Registration,
6
+ } from '@salesforce/lds-adapters-uiapi';
7
+ import { expect } from '@jest/globals';
8
+
9
+ import AdsBridge from '../ads-bridge';
10
+ import { isDMOEntity } from '../ads-bridge';
11
+ import { addObjectInfo, addRecord, createObjectInfo, createRecord } from './test-utils';
12
+ import { instrumentation } from '../instrumentation';
13
+ import { withRegistration } from '@salesforce/lds-default-luvio';
14
+
15
+ import { ingestDenormalizedRecordRepresentation } from '@salesforce/lds-runtime-mobile';
16
+
17
+ function createBridge() {
18
+ withRegistration<Registration>('@salesforce/lds-adapters-uiapi', (registration) => {
19
+ const { configuration } = registration;
20
+ configuration.setRecordRepresentationIngestionOverride(
21
+ ingestDenormalizedRecordRepresentation
22
+ );
23
+ });
24
+
25
+ const store = new InMemoryStore();
26
+ const environment = new Environment(store, jest.fn());
27
+ const luvio = new Luvio(environment);
28
+ const bridge = new AdsBridge(luvio, getRecordIngestionOverride());
29
+
30
+ return { store, luvio, bridge };
31
+ }
32
+
33
+ // constants for record type tests
34
+ const MAIN_RECORD_TYPE_ID = '012000000000000AAA';
35
+
36
+ const nonMasterRecordTypeInfo = {
37
+ available: true,
38
+ defaultRecordTypeMapping: true,
39
+ master: false,
40
+ name: 'non-master-record-type',
41
+ description: 'foo',
42
+ recordTypeId: 'non-master',
43
+ };
44
+
45
+ function queryRecord(luvio: Luvio, { recordId }: { recordId: string }): any {
46
+ return luvio.storeLookup({
47
+ recordId: keyBuilderRecord(luvio, { recordId }),
48
+ node: {
49
+ kind: 'Fragment',
50
+ private: [],
51
+ },
52
+ variables: {},
53
+ });
54
+ }
55
+
56
+ const timerMetricAddDurationSpy = jest.spyOn(instrumentation, 'timerMetricAddDuration');
57
+
58
+ beforeEach(() => {
59
+ (timerMetricAddDurationSpy as jest.Mock<any, any>).mockClear();
60
+ });
61
+
62
+ describe('AdsBridge', () => {
63
+ describe('addRecords', () => {
64
+ it('ingests all the records if no allowlist is provided', () => {
65
+ const { luvio, bridge } = createBridge();
66
+
67
+ bridge.addRecords([
68
+ createRecord({
69
+ id: '123',
70
+ apiName: 'Public',
71
+ }),
72
+ createRecord({
73
+ id: '456',
74
+ apiName: 'Secret',
75
+ }),
76
+ ]);
77
+
78
+ const { data: publicRecord } = queryRecord(luvio, { recordId: '123' });
79
+ expect(publicRecord.id).toBe('123');
80
+ const { data: secretRecord } = queryRecord(luvio, { recordId: '456' });
81
+ expect(secretRecord.id).toBe('456');
82
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ it('ingests the records unless they are explicitly not allowlisted', () => {
86
+ const { luvio, bridge } = createBridge();
87
+
88
+ bridge.addRecords(
89
+ [
90
+ createRecord({
91
+ id: '123',
92
+ apiName: 'Public',
93
+ }),
94
+ createRecord({
95
+ id: '456',
96
+ apiName: 'Secret',
97
+ }),
98
+ ],
99
+ {
100
+ Secret: 'false',
101
+ }
102
+ );
103
+
104
+ const { data: publicRecord } = queryRecord(luvio, { recordId: '123' });
105
+ expect(publicRecord.id).toBe('123');
106
+ const { data: secretRecord } = queryRecord(luvio, { recordId: '456' });
107
+ expect(secretRecord).toBe(undefined);
108
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ it("doesn't emit ingested records via the bridge", () => {
112
+ const { bridge } = createBridge();
113
+
114
+ const fn = jest.fn();
115
+ bridge.receiveFromLdsCallback = fn;
116
+
117
+ bridge.addRecords([
118
+ createRecord({
119
+ id: '123',
120
+ apiName: 'Public',
121
+ }),
122
+ createRecord({
123
+ id: '456',
124
+ apiName: 'Secret',
125
+ }),
126
+ ]);
127
+
128
+ expect(fn).toHaveBeenCalledTimes(0);
129
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
130
+ });
131
+
132
+ it('keeps the original record intact', () => {
133
+ const { bridge } = createBridge();
134
+
135
+ const config = {
136
+ id: '123',
137
+ apiName: 'Public',
138
+ fields: {
139
+ Id: {
140
+ value: '123',
141
+ displayValue: null,
142
+ },
143
+ },
144
+ };
145
+ const record = createRecord(config);
146
+ const recordCopy = JSON.parse(JSON.stringify(record));
147
+
148
+ bridge.addRecords([record]);
149
+
150
+ expect(record).toEqual(recordCopy);
151
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
152
+ });
153
+
154
+ it('does not let master record type overwrite non-master record type', () => {
155
+ const { bridge, luvio } = createBridge();
156
+
157
+ addRecord(
158
+ luvio,
159
+ createRecord({
160
+ id: '123',
161
+ recordTypeId: nonMasterRecordTypeInfo.recordTypeId,
162
+ recordTypeInfo: nonMasterRecordTypeInfo,
163
+ })
164
+ );
165
+
166
+ bridge.addRecords([
167
+ createRecord({
168
+ id: '123',
169
+ recordTypeId: MAIN_RECORD_TYPE_ID,
170
+ recordTypeInfo: null,
171
+ }),
172
+ ]);
173
+
174
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
175
+ data: {
176
+ recordTypeId: nonMasterRecordTypeInfo.recordTypeId,
177
+ recordTypeInfo: nonMasterRecordTypeInfo,
178
+ },
179
+ });
180
+ });
181
+
182
+ it('lets non-master record type overwrie master record type', () => {
183
+ const { bridge, luvio } = createBridge();
184
+
185
+ addRecord(
186
+ luvio,
187
+ createRecord({
188
+ id: '123',
189
+ recordTypeId: MAIN_RECORD_TYPE_ID,
190
+ recordTypeInfo: null,
191
+ })
192
+ );
193
+
194
+ bridge.addRecords([
195
+ createRecord({
196
+ id: '123',
197
+ recordTypeId: nonMasterRecordTypeInfo.recordTypeId,
198
+ recordTypeInfo: nonMasterRecordTypeInfo,
199
+ }),
200
+ ]);
201
+
202
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
203
+ data: {
204
+ recordTypeId: nonMasterRecordTypeInfo.recordTypeId,
205
+ recordTypeInfo: nonMasterRecordTypeInfo,
206
+ },
207
+ });
208
+ });
209
+
210
+ it('recurses over nested records when checking record types', () => {
211
+ const { bridge, luvio } = createBridge();
212
+
213
+ addRecord(
214
+ luvio,
215
+ createRecord({
216
+ id: '123',
217
+ recordTypeId: MAIN_RECORD_TYPE_ID,
218
+ recordTypeInfo: null,
219
+ fields: {
220
+ Child: {
221
+ displayValue: null,
222
+ value: createRecord({
223
+ id: '456',
224
+ recordTypeId: MAIN_RECORD_TYPE_ID,
225
+ recordTypeInfo: null,
226
+ }),
227
+ },
228
+ },
229
+ })
230
+ );
231
+
232
+ bridge.addRecords([
233
+ // same as above, except child now has non-master record type
234
+ createRecord({
235
+ id: '123',
236
+ recordTypeId: MAIN_RECORD_TYPE_ID,
237
+ recordTypeInfo: null,
238
+ fields: {
239
+ Child: {
240
+ displayValue: null,
241
+ value: createRecord({
242
+ id: '456',
243
+ recordTypeId: nonMasterRecordTypeInfo.recordTypeId,
244
+ recordTypeInfo: nonMasterRecordTypeInfo,
245
+ }),
246
+ },
247
+ },
248
+ }),
249
+ ]);
250
+
251
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
252
+ data: {
253
+ recordTypeId: MAIN_RECORD_TYPE_ID,
254
+ recordTypeInfo: null,
255
+ fields: {
256
+ Child: {
257
+ value: {
258
+ recordTypeId: nonMasterRecordTypeInfo.recordTypeId,
259
+ recordTypeInfo: nonMasterRecordTypeInfo,
260
+ },
261
+ },
262
+ },
263
+ },
264
+ });
265
+ });
266
+
267
+ describe('record refresh', () => {
268
+ it('should refresh updated record when merge conflicts', () => {
269
+ const { bridge, luvio } = createBridge();
270
+ addRecord(
271
+ luvio,
272
+ createRecord({
273
+ id: '123',
274
+ weakEtag: 1,
275
+ fields: {
276
+ Foo: {
277
+ value: 'foo',
278
+ displayValue: null,
279
+ },
280
+ Bar: {
281
+ value: 'bar',
282
+ displayValue: null,
283
+ },
284
+ },
285
+ })
286
+ );
287
+
288
+ luvio.dispatchResourceRequest = jest.fn().mockResolvedValueOnce({
289
+ body: createRecord({
290
+ id: '123',
291
+ weakEtag: 2,
292
+ fields: {
293
+ Foo: {
294
+ value: 'foo',
295
+ displayValue: null,
296
+ },
297
+ Bar: {
298
+ value: 'bar2',
299
+ displayValue: null,
300
+ },
301
+ },
302
+ }),
303
+ });
304
+
305
+ bridge.addRecords([
306
+ createRecord({
307
+ id: '123',
308
+ weakEtag: 2,
309
+ fields: {
310
+ Bar: {
311
+ value: 'bar2',
312
+ displayValue: null,
313
+ },
314
+ },
315
+ }),
316
+ ]);
317
+
318
+ const record = queryRecord(luvio, { recordId: '123' });
319
+ expect(record).toMatchObject({
320
+ data: {
321
+ fields: {
322
+ Bar: {
323
+ value: 'bar2',
324
+ displayValue: null,
325
+ },
326
+ Foo: {
327
+ __state: {
328
+ pending: true,
329
+ },
330
+ },
331
+ },
332
+ weakEtag: 2,
333
+ },
334
+ });
335
+ expect(luvio.dispatchResourceRequest).toHaveBeenCalledTimes(1);
336
+ expect(luvio.dispatchResourceRequest).toHaveBeenCalledWith(
337
+ expect.objectContaining({
338
+ basePath: '/ui-api/records/123',
339
+ method: 'get',
340
+ }),
341
+ undefined
342
+ );
343
+ });
344
+
345
+ it('should refresh updated spanning record when merge conflicts', () => {
346
+ const { bridge, luvio } = createBridge();
347
+ addRecord(
348
+ luvio,
349
+ createRecord({
350
+ id: '456',
351
+ weakEtag: 1,
352
+ fields: {
353
+ Foo: {
354
+ value: 'foo',
355
+ displayValue: null,
356
+ },
357
+ Bar: {
358
+ value: 'bar',
359
+ displayValue: null,
360
+ },
361
+ },
362
+ })
363
+ );
364
+
365
+ luvio.dispatchResourceRequest = jest.fn().mockResolvedValueOnce({
366
+ body: createRecord({
367
+ id: '456',
368
+ weakEtag: 2,
369
+ fields: {
370
+ Foo: {
371
+ value: 'foo',
372
+ displayValue: null,
373
+ },
374
+ Bar: {
375
+ value: 'bar2',
376
+ displayValue: null,
377
+ },
378
+ },
379
+ }),
380
+ });
381
+
382
+ bridge.addRecords([
383
+ createRecord({
384
+ id: '123',
385
+ fields: {
386
+ Child: {
387
+ displayValue: null,
388
+ value: createRecord({
389
+ id: '456',
390
+ weakEtag: 2,
391
+ fields: {
392
+ Bar: {
393
+ value: 'bar2',
394
+ displayValue: null,
395
+ },
396
+ },
397
+ }),
398
+ },
399
+ },
400
+ }),
401
+ ]);
402
+
403
+ const record = queryRecord(luvio, { recordId: '456' });
404
+ expect(record).toMatchObject({
405
+ data: {
406
+ fields: {
407
+ Bar: {
408
+ value: 'bar2',
409
+ displayValue: null,
410
+ },
411
+ Foo: {
412
+ __state: {
413
+ pending: true,
414
+ },
415
+ },
416
+ },
417
+ weakEtag: 2,
418
+ },
419
+ });
420
+ expect(luvio.dispatchResourceRequest).toHaveBeenCalledTimes(1);
421
+ expect(luvio.dispatchResourceRequest).toHaveBeenCalledWith(
422
+ expect.objectContaining({
423
+ basePath: '/ui-api/records/456',
424
+ method: 'get',
425
+ }),
426
+ undefined
427
+ );
428
+ });
429
+ });
430
+
431
+ describe('displayValue', () => {
432
+ it('does not let null overwrite non-null displayValue', () => {
433
+ const { bridge, luvio } = createBridge();
434
+
435
+ addRecord(
436
+ luvio,
437
+ createRecord({
438
+ id: '123',
439
+ fields: {
440
+ Child: {
441
+ displayValue: 'foo',
442
+ value: createRecord({
443
+ id: '456',
444
+ }),
445
+ },
446
+ },
447
+ })
448
+ );
449
+
450
+ bridge.addRecords([
451
+ createRecord({
452
+ id: '123',
453
+ fields: {
454
+ Child: {
455
+ displayValue: null,
456
+ value: createRecord({
457
+ id: '456',
458
+ }),
459
+ },
460
+ },
461
+ }),
462
+ ]);
463
+
464
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
465
+ data: {
466
+ fields: {
467
+ Child: {
468
+ displayValue: 'foo',
469
+ value: {
470
+ id: '456',
471
+ },
472
+ },
473
+ },
474
+ },
475
+ });
476
+ });
477
+
478
+ it('allows to set displayValue when existing field has null value', () => {
479
+ const { bridge, luvio } = createBridge();
480
+
481
+ addRecord(
482
+ luvio,
483
+ createRecord({
484
+ id: '123',
485
+ fields: {
486
+ Child: {
487
+ displayValue: null,
488
+ value: null,
489
+ },
490
+ },
491
+ })
492
+ );
493
+
494
+ bridge.addRecords([
495
+ createRecord({
496
+ id: '123',
497
+ fields: {
498
+ Child: {
499
+ displayValue: 'foo',
500
+ value: createRecord({
501
+ id: '456',
502
+ }),
503
+ },
504
+ },
505
+ }),
506
+ ]);
507
+
508
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
509
+ data: {
510
+ fields: {
511
+ Child: {
512
+ displayValue: 'foo',
513
+ value: {
514
+ id: '456',
515
+ },
516
+ },
517
+ },
518
+ },
519
+ });
520
+ });
521
+
522
+ it('allows to set new displayValue from incoming field has a non-null displayValue', () => {
523
+ const { bridge, luvio } = createBridge();
524
+
525
+ addRecord(
526
+ luvio,
527
+ createRecord({
528
+ id: '123',
529
+ fields: {
530
+ Child: {
531
+ displayValue: 'foo',
532
+ value: createRecord({
533
+ id: '456',
534
+ }),
535
+ },
536
+ },
537
+ })
538
+ );
539
+
540
+ bridge.addRecords([
541
+ createRecord({
542
+ id: '123',
543
+ fields: {
544
+ Child: {
545
+ displayValue: 'bar',
546
+ value: createRecord({
547
+ id: '789',
548
+ }),
549
+ },
550
+ },
551
+ }),
552
+ ]);
553
+
554
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
555
+ data: {
556
+ fields: {
557
+ Child: {
558
+ displayValue: 'bar',
559
+ value: {
560
+ id: '789',
561
+ },
562
+ },
563
+ },
564
+ },
565
+ });
566
+ });
567
+
568
+ it('allows to set displayValue to null when incoming field has null value ', () => {
569
+ const { bridge, luvio } = createBridge();
570
+
571
+ addRecord(
572
+ luvio,
573
+ createRecord({
574
+ id: '123',
575
+ fields: {
576
+ Child: {
577
+ displayValue: 'foo',
578
+ value: createRecord({
579
+ id: '456',
580
+ }),
581
+ },
582
+ },
583
+ })
584
+ );
585
+
586
+ bridge.addRecords([
587
+ createRecord({
588
+ id: '123',
589
+ fields: {
590
+ Child: {
591
+ displayValue: null,
592
+ value: null,
593
+ },
594
+ },
595
+ }),
596
+ ]);
597
+
598
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({
599
+ data: {
600
+ fields: {
601
+ Child: {
602
+ displayValue: null,
603
+ value: null,
604
+ },
605
+ },
606
+ },
607
+ });
608
+ });
609
+ });
610
+ });
611
+
612
+ describe('evict', () => {
613
+ it('should remove an existing record if there is one', async () => {
614
+ const { bridge, luvio } = createBridge();
615
+
616
+ addRecord(
617
+ luvio,
618
+ createRecord({
619
+ id: '123',
620
+ })
621
+ );
622
+
623
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({ state: 'Fulfilled' });
624
+ await bridge.evict('123');
625
+ expect(queryRecord(luvio, { recordId: '123' })).toMatchObject({ state: 'Unfulfilled' });
626
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
627
+ });
628
+
629
+ it('should not emit when a record has been deleted via the bridge', async () => {
630
+ const { bridge, luvio } = createBridge();
631
+
632
+ addRecord(
633
+ luvio,
634
+ createRecord({
635
+ id: '123',
636
+ })
637
+ );
638
+
639
+ const fn = jest.fn();
640
+ bridge.receiveFromLdsCallback = fn;
641
+
642
+ await bridge.evict('123');
643
+
644
+ expect(fn).not.toHaveBeenCalled();
645
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
646
+ });
647
+ });
648
+
649
+ describe('getTrackedFieldsForRecord', () => {
650
+ it('returns the fields associated with a record', async () => {
651
+ const { bridge } = createBridge();
652
+
653
+ bridge.addRecords([
654
+ createRecord({
655
+ id: '123',
656
+ apiName: 'Test__c',
657
+ fields: {
658
+ Id: { displayValue: null, value: '123' },
659
+ Name: { displayValue: null, value: 'Test' },
660
+ Amount: { displayValue: null, value: 123 },
661
+ },
662
+ }),
663
+ ]);
664
+
665
+ const fields = await bridge.getTrackedFieldsForRecord('123');
666
+ expect(fields).toEqual(['Test__c.Id', 'Test__c.Name', 'Test__c.Amount']);
667
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
668
+ });
669
+
670
+ it("doesn't return the spanning records fields", async () => {
671
+ const { bridge } = createBridge();
672
+
673
+ bridge.addRecords([
674
+ createRecord({
675
+ id: '123',
676
+ apiName: 'Test__c',
677
+ fields: {
678
+ Id: { displayValue: null, value: '123' },
679
+ Child: {
680
+ displayValue: null,
681
+ value: createRecord({
682
+ id: '456',
683
+ apiName: 'Child__c',
684
+ fields: {
685
+ Id: { displayValue: null, value: '456' },
686
+ Name: { displayValue: null, value: 'Child' },
687
+ },
688
+ }),
689
+ },
690
+ },
691
+ }),
692
+ ]);
693
+
694
+ const fields = await bridge.getTrackedFieldsForRecord('123');
695
+ expect(fields).toEqual(['Test__c.Id', 'Test__c.Child']);
696
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
697
+ });
698
+ });
699
+
700
+ describe('receiveFromLdsCallback', () => {
701
+ it('emits when a record is ingested', () => {
702
+ const { bridge, luvio } = createBridge();
703
+
704
+ const fn = jest.fn();
705
+ bridge.receiveFromLdsCallback = fn;
706
+
707
+ addRecord(
708
+ luvio,
709
+ createRecord({
710
+ id: '123456',
711
+ apiName: 'Test__c',
712
+ fields: {
713
+ Id: { displayValue: null, value: '123456' },
714
+ },
715
+ })
716
+ );
717
+
718
+ expect(fn).toHaveBeenCalledTimes(1);
719
+ expect(fn).toHaveBeenCalledWith(
720
+ {
721
+ '123456': {
722
+ Test__c: {
723
+ isPrimary: true,
724
+ record: expect.objectContaining({
725
+ id: '123456',
726
+ apiName: 'Test__c',
727
+ fields: {
728
+ Id: { displayValue: null, value: '123456' },
729
+ },
730
+ }),
731
+ },
732
+ },
733
+ },
734
+ expect.any(Object)
735
+ );
736
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
737
+ });
738
+
739
+ it('does not emit when a record is evicted from luvio', () => {
740
+ const { bridge, luvio } = createBridge();
741
+
742
+ const fn = jest.fn();
743
+ bridge.receiveFromLdsCallback = fn;
744
+ const mockRecordId = '123456';
745
+ addRecord(
746
+ luvio,
747
+ createRecord({
748
+ id: mockRecordId,
749
+ apiName: 'Test__c',
750
+ fields: {
751
+ Id: { displayValue: null, value: '123456' },
752
+ },
753
+ })
754
+ );
755
+
756
+ luvio.storeEvict(keyBuilderRecord(luvio, { recordId: mockRecordId }));
757
+ luvio.storeBroadcast();
758
+
759
+ // verify mocked callback only called once for addRecord
760
+ expect(fn).toHaveBeenCalledTimes(1);
761
+ });
762
+
763
+ it('does not emit when a record is ingested then evicted prior to broadcast', () => {
764
+ const { bridge, luvio } = createBridge();
765
+
766
+ const fn = jest.fn();
767
+ bridge.receiveFromLdsCallback = fn;
768
+ const mockRecordId = '123456';
769
+ const mockRecord = createRecord({
770
+ id: mockRecordId,
771
+ apiName: 'Test__c',
772
+ fields: {
773
+ Id: { displayValue: null, value: '123456' },
774
+ },
775
+ });
776
+
777
+ luvio.storeIngest('', getRecordIngestionOverride(), mockRecord);
778
+ luvio.storeEvict(keyBuilderRecord(luvio, { recordId: mockRecordId }));
779
+ luvio.storeBroadcast();
780
+
781
+ expect(fn).toHaveBeenCalledTimes(0);
782
+ });
783
+
784
+ it('emits a new field ingested into an existed record', () => {
785
+ const { bridge, luvio } = createBridge();
786
+
787
+ addRecord(
788
+ luvio,
789
+ createRecord({
790
+ id: '123456',
791
+ apiName: 'Test__c',
792
+ fields: {
793
+ Id: { displayValue: null, value: '123456' },
794
+ },
795
+ })
796
+ );
797
+
798
+ const fn = jest.fn();
799
+ bridge.receiveFromLdsCallback = fn;
800
+
801
+ addRecord(
802
+ luvio,
803
+ createRecord({
804
+ id: '123456',
805
+ apiName: 'Test__c',
806
+ fields: {
807
+ Name: { displayValue: null, value: 'Test' },
808
+ },
809
+ })
810
+ );
811
+
812
+ expect(fn).toHaveBeenCalledTimes(1);
813
+ expect(fn).toHaveBeenCalledWith(
814
+ {
815
+ '123456': {
816
+ Test__c: {
817
+ isPrimary: true,
818
+ record: expect.objectContaining({
819
+ id: '123456',
820
+ apiName: 'Test__c',
821
+ fields: {
822
+ Id: { displayValue: null, value: '123456' },
823
+ Name: { displayValue: null, value: 'Test' },
824
+ },
825
+ }),
826
+ },
827
+ },
828
+ },
829
+ expect.any(Object)
830
+ );
831
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
832
+ });
833
+
834
+ it('emits all the ingested records', () => {
835
+ const { bridge, luvio } = createBridge();
836
+
837
+ const fn = jest.fn();
838
+ bridge.receiveFromLdsCallback = fn;
839
+
840
+ addRecord(
841
+ luvio,
842
+ createRecord({
843
+ id: '123',
844
+ fields: {
845
+ Id: { displayValue: null, value: '123' },
846
+ CreatedBy: {
847
+ displayValue: null,
848
+ value: createRecord({
849
+ id: '456',
850
+ fields: {
851
+ Id: { displayValue: null, value: '456' },
852
+ },
853
+ }),
854
+ },
855
+ },
856
+ })
857
+ );
858
+
859
+ expect(fn).toHaveBeenCalledTimes(1);
860
+ expect(fn).toHaveBeenCalledWith(
861
+ {
862
+ '123': {
863
+ Test__c: {
864
+ isPrimary: true,
865
+ record: expect.objectContaining({
866
+ id: '123',
867
+ apiName: 'Test__c',
868
+ fields: {
869
+ Id: { displayValue: null, value: '123' },
870
+ CreatedBy: {
871
+ displayValue: null,
872
+ value: expect.objectContaining({
873
+ id: '456',
874
+ apiName: 'Test__c',
875
+ fields: {},
876
+ }),
877
+ },
878
+ },
879
+ }),
880
+ },
881
+ },
882
+ '456': {
883
+ Test__c: {
884
+ isPrimary: true,
885
+ record: expect.objectContaining({
886
+ id: '456',
887
+ apiName: 'Test__c',
888
+ fields: {
889
+ Id: { displayValue: null, value: '456' },
890
+ },
891
+ }),
892
+ },
893
+ },
894
+ },
895
+ expect.any(Object)
896
+ );
897
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
898
+ });
899
+
900
+ it('emits records with circular dependencies stripped out', () => {
901
+ const { bridge, luvio } = createBridge();
902
+
903
+ const fn = jest.fn();
904
+ bridge.receiveFromLdsCallback = fn;
905
+
906
+ addRecord(
907
+ luvio,
908
+ createRecord({
909
+ id: '123456',
910
+ fields: {
911
+ CreatedBy: {
912
+ displayValue: null,
913
+ value: createRecord({
914
+ id: '123456',
915
+ fields: {
916
+ Id: { displayValue: null, value: '123456' },
917
+ },
918
+ }),
919
+ },
920
+ },
921
+ })
922
+ );
923
+
924
+ expect(fn).toHaveBeenCalledTimes(1);
925
+ expect(fn).toHaveBeenCalledWith(
926
+ {
927
+ '123456': {
928
+ Test__c: {
929
+ isPrimary: true,
930
+ record: expect.objectContaining({
931
+ id: '123456',
932
+ apiName: 'Test__c',
933
+ fields: {
934
+ Id: { displayValue: null, value: '123456' },
935
+ CreatedBy: {
936
+ displayValue: null,
937
+ value: expect.objectContaining({
938
+ id: '123456',
939
+ apiName: 'Test__c',
940
+ fields: {},
941
+ }),
942
+ },
943
+ },
944
+ }),
945
+ },
946
+ },
947
+ },
948
+ expect.any(Object)
949
+ );
950
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
951
+ });
952
+
953
+ it('emits stripped down spanning records when spanning record value is not present', () => {
954
+ const { bridge, luvio } = createBridge();
955
+
956
+ addRecord(
957
+ luvio,
958
+ createRecord({
959
+ id: '123456',
960
+ apiName: 'Opportunity',
961
+ fields: {
962
+ Account: {
963
+ displayValue: null,
964
+ value: createRecord({
965
+ id: '789',
966
+ apiName: 'Account',
967
+ fields: {
968
+ Id: { displayValue: null, value: '789' },
969
+ },
970
+ }),
971
+ },
972
+ },
973
+ })
974
+ );
975
+
976
+ luvio.storeEvict(keyBuilderRecord(luvio, { recordId: '789' }));
977
+ const fn = jest.fn();
978
+ bridge.receiveFromLdsCallback = fn;
979
+
980
+ addRecord(
981
+ luvio,
982
+ createRecord({
983
+ id: '123456',
984
+ apiName: 'Opportunity',
985
+ fields: {
986
+ Id: { displayValue: null, value: '123456' },
987
+ },
988
+ })
989
+ );
990
+
991
+ expect(fn).toHaveBeenCalledTimes(1);
992
+ expect(fn).toHaveBeenCalledWith(
993
+ {
994
+ '123456': {
995
+ Opportunity: {
996
+ isPrimary: true,
997
+ record: expect.objectContaining({
998
+ id: '123456',
999
+ apiName: 'Opportunity',
1000
+ fields: {
1001
+ Id: { displayValue: null, value: '123456' },
1002
+ },
1003
+ }),
1004
+ },
1005
+ },
1006
+ },
1007
+ expect.any(Object)
1008
+ );
1009
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1010
+ });
1011
+
1012
+ it('does not emit data when spanning record value is pending', () => {
1013
+ const { bridge, luvio, store } = createBridge();
1014
+
1015
+ addRecord(
1016
+ luvio,
1017
+ createRecord({
1018
+ id: '123',
1019
+ apiName: 'Opportunity',
1020
+ fields: {
1021
+ Account: {
1022
+ displayValue: null,
1023
+ value: createRecord({
1024
+ id: '456',
1025
+ apiName: 'Account',
1026
+ fields: {},
1027
+ }),
1028
+ },
1029
+ },
1030
+ })
1031
+ );
1032
+
1033
+ // Mark the ingested record in the store as pending.
1034
+ store.fallbackStringKeyInMemoryStore.records[
1035
+ keyBuilderRecord(luvio, { recordId: '123' })
1036
+ ].fields['Account'] = {
1037
+ __ref: undefined,
1038
+ __state: {
1039
+ pending: true,
1040
+ },
1041
+ };
1042
+
1043
+ const fn = jest.fn();
1044
+ bridge.receiveFromLdsCallback = fn;
1045
+
1046
+ // Trigger the bridge emit flow once on the record with the pending field by adding a
1047
+ // new field to the same record.
1048
+ addRecord(
1049
+ luvio,
1050
+ createRecord({
1051
+ id: '123',
1052
+ apiName: 'Opportunity',
1053
+ fields: {
1054
+ Id: { displayValue: null, value: '123' },
1055
+ },
1056
+ })
1057
+ );
1058
+
1059
+ expect(fn).not.toHaveBeenCalled();
1060
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1061
+ });
1062
+
1063
+ it('does not emit data when record has a pending field', () => {
1064
+ const { bridge, luvio, store } = createBridge();
1065
+
1066
+ addRecord(
1067
+ luvio,
1068
+ createRecord({
1069
+ id: '123',
1070
+ apiName: 'Opportunity',
1071
+ fields: {
1072
+ Id: {
1073
+ displayValue: null,
1074
+ value: '123',
1075
+ },
1076
+ },
1077
+ })
1078
+ );
1079
+
1080
+ // Mark the Id field of the ingested record in the store as pending.
1081
+ const recordKey = keyBuilderRecord(luvio, { recordId: '123' });
1082
+ store.fallbackStringKeyInMemoryStore.records[recordKey].fields.Id = {
1083
+ __ref: undefined,
1084
+ __state: {
1085
+ pending: true,
1086
+ },
1087
+ };
1088
+
1089
+ const fn = jest.fn();
1090
+ bridge.receiveFromLdsCallback = fn;
1091
+
1092
+ // add a new record to trigger the bridge emit flow
1093
+ addRecord(
1094
+ luvio,
1095
+ createRecord({
1096
+ id: '123',
1097
+ apiName: 'Opportunity',
1098
+ fields: {
1099
+ Name: { displayValue: null, value: 'abc' },
1100
+ },
1101
+ })
1102
+ );
1103
+
1104
+ expect(fn).not.toHaveBeenCalled();
1105
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1106
+ });
1107
+
1108
+ it('emits stripped down spanning records when spanning record value is isMissing', () => {
1109
+ const { bridge, luvio, store } = createBridge();
1110
+
1111
+ addRecord(
1112
+ luvio,
1113
+ createRecord({
1114
+ id: '123',
1115
+ apiName: 'Opportunity',
1116
+ fields: {
1117
+ Account: {
1118
+ displayValue: null,
1119
+ value: createRecord({
1120
+ id: '456',
1121
+ apiName: 'Account',
1122
+ fields: {},
1123
+ }),
1124
+ },
1125
+ },
1126
+ })
1127
+ );
1128
+
1129
+ // Mark the ingested record in the store as missing
1130
+ store.fallbackStringKeyInMemoryStore.records[
1131
+ keyBuilderRecord(luvio, { recordId: '123' })
1132
+ ].fields['Account'] = {
1133
+ __ref: undefined,
1134
+ __state: {
1135
+ isMissing: true,
1136
+ },
1137
+ };
1138
+
1139
+ const fn = jest.fn();
1140
+ bridge.receiveFromLdsCallback = fn;
1141
+
1142
+ // Trigger the bridge emit flow once on the record with the missing field by adding a
1143
+ // new field to the same record.
1144
+ addRecord(
1145
+ luvio,
1146
+ createRecord({
1147
+ id: '123',
1148
+ apiName: 'Opportunity',
1149
+ fields: {
1150
+ Id: { displayValue: null, value: '123' },
1151
+ },
1152
+ })
1153
+ );
1154
+
1155
+ expect(fn).toHaveBeenCalledWith(
1156
+ {
1157
+ '123': {
1158
+ Opportunity: {
1159
+ isPrimary: true,
1160
+ record: expect.objectContaining({
1161
+ id: '123',
1162
+ apiName: 'Opportunity',
1163
+ fields: {
1164
+ Id: { displayValue: null, value: '123' },
1165
+ },
1166
+ }),
1167
+ },
1168
+ },
1169
+ },
1170
+ expect.any(Object)
1171
+ );
1172
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1173
+ });
1174
+
1175
+ it('extracts the object metadata from the record if the object info is missing', () => {
1176
+ const { bridge, luvio } = createBridge();
1177
+
1178
+ const fn = jest.fn();
1179
+ bridge.receiveFromLdsCallback = fn;
1180
+
1181
+ addRecord(
1182
+ luvio,
1183
+ createRecord({
1184
+ id: '123456',
1185
+ apiName: 'Test__c',
1186
+ })
1187
+ );
1188
+
1189
+ expect(fn).toHaveBeenCalledTimes(1);
1190
+ expect(fn).toHaveBeenCalledWith(expect.any(Object), {
1191
+ Test__c: {
1192
+ _entityLabel: 'Test__c',
1193
+ _keyPrefix: '123',
1194
+ _nameField: 'Name',
1195
+ },
1196
+ });
1197
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1198
+ });
1199
+
1200
+ it('passes null keyPrefix from the record if the object info keyPrefix is null', () => {
1201
+ const { bridge, luvio } = createBridge();
1202
+
1203
+ addObjectInfo(
1204
+ luvio,
1205
+ createObjectInfo({
1206
+ apiName: 'User',
1207
+ associateEntityType: null,
1208
+ associateParentEntity: null,
1209
+ keyPrefix: null,
1210
+ label: 'User',
1211
+ nameFields: ['Name'],
1212
+ })
1213
+ );
1214
+
1215
+ const fn = jest.fn();
1216
+ bridge.receiveFromLdsCallback = fn;
1217
+
1218
+ addRecord(
1219
+ luvio,
1220
+ createRecord({
1221
+ id: '123456',
1222
+ apiName: 'User',
1223
+ })
1224
+ );
1225
+
1226
+ expect(fn).toHaveBeenCalledTimes(1);
1227
+ expect(fn).toHaveBeenCalledWith(expect.any(Object), {
1228
+ User: {
1229
+ _entityLabel: 'User',
1230
+ _keyPrefix: null,
1231
+ _nameField: 'Name',
1232
+ },
1233
+ });
1234
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1235
+ });
1236
+
1237
+ it('extracts the object metadata from the object info if present', () => {
1238
+ const { bridge, luvio } = createBridge();
1239
+
1240
+ addObjectInfo(
1241
+ luvio,
1242
+ createObjectInfo({
1243
+ apiName: 'Test__c',
1244
+ associateEntityType: null,
1245
+ associateParentEntity: null,
1246
+ keyPrefix: 'TEST',
1247
+ label: 'Test',
1248
+ nameFields: ['Name'],
1249
+ })
1250
+ );
1251
+
1252
+ const fn = jest.fn();
1253
+ bridge.receiveFromLdsCallback = fn;
1254
+
1255
+ addRecord(
1256
+ luvio,
1257
+ createRecord({
1258
+ id: '123456',
1259
+ apiName: 'Test__c',
1260
+ })
1261
+ );
1262
+
1263
+ expect(fn).toHaveBeenCalledTimes(1);
1264
+ expect(fn).toHaveBeenCalledWith(expect.any(Object), {
1265
+ Test__c: {
1266
+ _entityLabel: 'Test',
1267
+ _keyPrefix: 'TEST',
1268
+ _nameField: 'Name',
1269
+ },
1270
+ });
1271
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1272
+ });
1273
+
1274
+ it('extracts the first field name if multiple are provided', () => {
1275
+ const { bridge, luvio } = createBridge();
1276
+
1277
+ addObjectInfo(
1278
+ luvio,
1279
+ createObjectInfo({
1280
+ apiName: 'Test__c',
1281
+ associateEntityType: null,
1282
+ associateParentEntity: null,
1283
+ keyPrefix: 'TEST',
1284
+ label: 'Test',
1285
+ nameFields: ['First_Name', 'Last_Name'],
1286
+ })
1287
+ );
1288
+
1289
+ const fn = jest.fn();
1290
+ bridge.receiveFromLdsCallback = fn;
1291
+
1292
+ addRecord(
1293
+ luvio,
1294
+ createRecord({
1295
+ id: '123456',
1296
+ apiName: 'Test__c',
1297
+ })
1298
+ );
1299
+
1300
+ expect(fn).toHaveBeenCalledTimes(1);
1301
+ expect(fn).toHaveBeenCalledWith(expect.any(Object), {
1302
+ Test__c: {
1303
+ _entityLabel: 'Test',
1304
+ _keyPrefix: 'TEST',
1305
+ _nameField: 'First_Name',
1306
+ },
1307
+ });
1308
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1309
+ });
1310
+
1311
+ it('stops emitting if the function is replaced with undefined', () => {
1312
+ const { bridge, luvio } = createBridge();
1313
+
1314
+ const fn = jest.fn();
1315
+ bridge.receiveFromLdsCallback = fn;
1316
+ bridge.receiveFromLdsCallback = undefined;
1317
+
1318
+ addRecord(
1319
+ luvio,
1320
+ createRecord({
1321
+ id: '123',
1322
+ apiName: 'Test__c',
1323
+ fields: {
1324
+ Id: { displayValue: null, value: '123' },
1325
+ },
1326
+ })
1327
+ );
1328
+
1329
+ expect(fn).not.toHaveBeenCalled();
1330
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(0);
1331
+ });
1332
+
1333
+ it('does not emit when a DMO record is ingested', () => {
1334
+ const { bridge, luvio } = createBridge();
1335
+
1336
+ const fn = jest.fn();
1337
+ bridge.receiveFromLdsCallback = fn;
1338
+
1339
+ addRecord(
1340
+ luvio,
1341
+ createRecord({
1342
+ id: '123456',
1343
+ apiName: 'Account__dlm',
1344
+ fields: {
1345
+ Id: { displayValue: null, value: '123456' },
1346
+ },
1347
+ })
1348
+ );
1349
+
1350
+ expect(fn).toHaveBeenCalledTimes(0);
1351
+ expect(timerMetricAddDurationSpy).toHaveBeenCalledTimes(1);
1352
+ });
1353
+ });
1354
+
1355
+ describe('isDMOEntity', () => {
1356
+ it('should return true for DMO record', () => {
1357
+ const record = createRecord({
1358
+ id: '123456',
1359
+ apiName: 'Account__dlm',
1360
+ fields: {
1361
+ Id: { displayValue: null, value: '123456' },
1362
+ },
1363
+ });
1364
+ expect(isDMOEntity(record)).toBe(true);
1365
+ });
1366
+
1367
+ it('should return false for non-DMO record', () => {
1368
+ const record = createRecord({
1369
+ id: '123456',
1370
+ apiName: 'Account',
1371
+ fields: {
1372
+ Id: { displayValue: null, value: '123456' },
1373
+ },
1374
+ });
1375
+ expect(isDMOEntity(record)).toBe(false);
1376
+ });
1377
+ });
1378
+ });