@salesforce/lds-ads-bridge 1.124.2 → 1.124.3
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.
- package/dist/ads-bridge-perf.js +4728 -4728
- package/dist/adsBridge.js +342 -342
- package/package.json +2 -2
- package/rollup.config.js +3 -3
- package/dist/ads-bridge.d.ts +0 -87
- package/dist/instrumentation.d.ts +0 -22
- package/dist/main.d.ts +0 -13
- package/dist/utils/language.d.ts +0 -11
- package/dist/utils/metric-keys.d.ts +0 -3
package/dist/adsBridge.js
CHANGED
|
@@ -15,29 +15,29 @@
|
|
|
15
15
|
import { ingestRecord, keyBuilderRecord, keyBuilderObjectInfo } from 'force/ldsAdaptersUiapi';
|
|
16
16
|
import { withDefaultLuvio } from 'force/ldsEngine';
|
|
17
17
|
|
|
18
|
-
const { push } = Array.prototype;
|
|
19
|
-
const { keys } = Object;
|
|
20
|
-
const { hasOwnProperty } = Object.prototype;
|
|
18
|
+
const { push } = Array.prototype;
|
|
19
|
+
const { keys } = Object;
|
|
20
|
+
const { hasOwnProperty } = Object.prototype;
|
|
21
21
|
const { parse, stringify } = JSON;
|
|
22
22
|
|
|
23
|
-
const ADS_BRIDGE_ADD_RECORDS_DURATION = 'ads-bridge-add-records-duration';
|
|
24
|
-
const ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION = 'ads-bridge-emit-record-changed-duration';
|
|
23
|
+
const ADS_BRIDGE_ADD_RECORDS_DURATION = 'ads-bridge-add-records-duration';
|
|
24
|
+
const ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION = 'ads-bridge-emit-record-changed-duration';
|
|
25
25
|
const ADS_BRIDGE_EVICT_DURATION = 'ads-bridge-evict-duration';
|
|
26
26
|
|
|
27
|
-
// For use by callers within this module to instrument interesting things.
|
|
28
|
-
let instrumentation = {
|
|
29
|
-
timerMetricAddDuration: (_metricName, _valueInMs) => { },
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Allows external modules (typically a runtime environment) to set
|
|
33
|
-
* instrumentation hooks for this module. Note that the hooks are
|
|
34
|
-
* incremental - hooks not suppiled in newInstrumentation will retain
|
|
35
|
-
* their previous values. The default instrumentation hooks are no-ops.
|
|
36
|
-
*
|
|
37
|
-
* @param newInstrumentation instrumentation hooks to be overridden
|
|
38
|
-
*/
|
|
39
|
-
function instrument(newInstrumentation) {
|
|
40
|
-
instrumentation = Object.assign(instrumentation, newInstrumentation);
|
|
27
|
+
// For use by callers within this module to instrument interesting things.
|
|
28
|
+
let instrumentation = {
|
|
29
|
+
timerMetricAddDuration: (_metricName, _valueInMs) => { },
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Allows external modules (typically a runtime environment) to set
|
|
33
|
+
* instrumentation hooks for this module. Note that the hooks are
|
|
34
|
+
* incremental - hooks not suppiled in newInstrumentation will retain
|
|
35
|
+
* their previous values. The default instrumentation hooks are no-ops.
|
|
36
|
+
*
|
|
37
|
+
* @param newInstrumentation instrumentation hooks to be overridden
|
|
38
|
+
*/
|
|
39
|
+
function instrument(newInstrumentation) {
|
|
40
|
+
instrumentation = Object.assign(instrumentation, newInstrumentation);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -46,333 +46,333 @@ function instrument(newInstrumentation) {
|
|
|
46
46
|
* For full license text, see the LICENSE.txt file
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
-
const API_NAMESPACE = 'UiApi';
|
|
50
|
-
const RECORD_REPRESENTATION_NAME = 'RecordRepresentation';
|
|
51
|
-
const RECORD_ID_PREFIX = `${API_NAMESPACE}::${RECORD_REPRESENTATION_NAME}:`;
|
|
52
|
-
const RECORD_FIELDS_KEY_JUNCTION = '__fields__';
|
|
53
|
-
function isStoreKeyRecordId(key) {
|
|
54
|
-
return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
|
|
49
|
+
const API_NAMESPACE = 'UiApi';
|
|
50
|
+
const RECORD_REPRESENTATION_NAME = 'RecordRepresentation';
|
|
51
|
+
const RECORD_ID_PREFIX = `${API_NAMESPACE}::${RECORD_REPRESENTATION_NAME}:`;
|
|
52
|
+
const RECORD_FIELDS_KEY_JUNCTION = '__fields__';
|
|
53
|
+
function isStoreKeyRecordId(key) {
|
|
54
|
+
return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// No need to pass the actual record key `luvio.ingestStore`. The `RecordRepresentation.ts#ingest`
|
|
58
|
-
// function extracts the appropriate record id from the ingested record.
|
|
59
|
-
const INGEST_KEY = '';
|
|
60
|
-
const MASTER_RECORD_TYPE_ID = '012000000000000AAA';
|
|
61
|
-
const DMO_API_NAME_SUFFIX = '__dlm';
|
|
62
|
-
function isGraphNode(node) {
|
|
63
|
-
return node !== null && node.type === 'Node';
|
|
64
|
-
}
|
|
65
|
-
function isSpanningRecord(fieldValue) {
|
|
66
|
-
return fieldValue !== null && typeof fieldValue === 'object';
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Returns a shallow copy of a record with its field values if it is a scalar and a reference and a
|
|
70
|
-
* a RecordRepresentation with no field if the value if a spanning record.
|
|
71
|
-
* It returns null if the record contains any pending field.
|
|
72
|
-
*/
|
|
73
|
-
function getShallowRecord(luvio, storeRecordId) {
|
|
74
|
-
const recordNode = luvio.getNode(storeRecordId);
|
|
75
|
-
if (!isGraphNode(recordNode)) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const fieldsCopy = {};
|
|
79
|
-
const copy = {
|
|
80
|
-
...recordNode.retrieve(),
|
|
81
|
-
fields: fieldsCopy,
|
|
82
|
-
childRelationships: {},
|
|
83
|
-
};
|
|
84
|
-
const fieldsNode = recordNode.object('fields');
|
|
85
|
-
const fieldNames = fieldsNode.keys();
|
|
86
|
-
for (let i = 0, len = fieldNames.length; i < len; i++) {
|
|
87
|
-
let fieldCopy;
|
|
88
|
-
const fieldName = fieldNames[i];
|
|
89
|
-
const fieldLink = fieldsNode.link(fieldName);
|
|
90
|
-
if (fieldLink.isPending() === true) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const fieldNode = fieldLink.follow();
|
|
94
|
-
if (!isGraphNode(fieldNode)) {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const { displayValue, value } = fieldNode.retrieve();
|
|
98
|
-
if (fieldNode.isScalar('value')) {
|
|
99
|
-
fieldCopy = {
|
|
100
|
-
displayValue: displayValue,
|
|
101
|
-
value: value,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
const spanningRecordLink = fieldNode.link('value');
|
|
106
|
-
if (spanningRecordLink.isPending() === true) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
const spanningRecordNode = spanningRecordLink.follow();
|
|
110
|
-
if (!isGraphNode(spanningRecordNode)) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
fieldCopy = {
|
|
114
|
-
displayValue,
|
|
115
|
-
value: {
|
|
116
|
-
...spanningRecordNode.retrieve(),
|
|
117
|
-
fields: {},
|
|
118
|
-
childRelationships: {},
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
fieldsCopy[fieldName] = fieldCopy;
|
|
123
|
-
}
|
|
124
|
-
return copy;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Returns the ADS object metadata representation for a specific record.
|
|
128
|
-
*/
|
|
129
|
-
function getObjectMetadata(luvio, record) {
|
|
130
|
-
const { data: objectInfo } = luvio.storeLookup({
|
|
131
|
-
recordId: keyBuilderObjectInfo(luvio, { apiName: record.apiName }),
|
|
132
|
-
node: {
|
|
133
|
-
kind: 'Fragment',
|
|
134
|
-
private: ['eTag'],
|
|
135
|
-
opaque: true,
|
|
136
|
-
},
|
|
137
|
-
variables: {},
|
|
138
|
-
});
|
|
139
|
-
if (objectInfo !== undefined) {
|
|
140
|
-
let nameField = 'Name';
|
|
141
|
-
// Extract the entity name field from the object info. In the case where there are multiple
|
|
142
|
-
// field names then pick up the first one.
|
|
143
|
-
if (objectInfo.nameFields.length !== 0 && objectInfo.nameFields.indexOf('Name') === -1) {
|
|
144
|
-
nameField = objectInfo.nameFields[0];
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
_nameField: nameField,
|
|
148
|
-
_entityLabel: objectInfo.label,
|
|
149
|
-
_keyPrefix: objectInfo.keyPrefix,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
_nameField: 'Name',
|
|
154
|
-
_entityLabel: record.apiName,
|
|
155
|
-
_keyPrefix: record.id.substring(0, 3),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* RecordGvp can send records back to ADS with record types incorrectly set to the master
|
|
160
|
-
* record type. Since there are no known legitimate scenarios where a record can change from a
|
|
161
|
-
* non-master record type back to the master record type, we assume such a transition
|
|
162
|
-
* indicates a RecordGvp mistake. This function checks for that scenario and overwrites the
|
|
163
|
-
* incoming ADS record type information with what we already have in the store when it
|
|
164
|
-
* occurs.
|
|
165
|
-
*
|
|
166
|
-
* @param luvio Luvio
|
|
167
|
-
* @param record record from ADS, will be fixed in situ
|
|
168
|
-
*/
|
|
169
|
-
function fixRecordTypes(luvio, record) {
|
|
170
|
-
// non-master record types should always be correct
|
|
171
|
-
if (record.recordTypeId === MASTER_RECORD_TYPE_ID) {
|
|
172
|
-
const key = keyBuilderRecord(luvio, { recordId: record.id });
|
|
173
|
-
const recordNode = luvio.getNode(key);
|
|
174
|
-
if (isGraphNode(recordNode) &&
|
|
175
|
-
recordNode.scalar('recordTypeId') !== MASTER_RECORD_TYPE_ID) {
|
|
176
|
-
// ignore bogus incoming record type information & keep what we have
|
|
177
|
-
record.recordTypeId = recordNode.scalar('recordTypeId');
|
|
178
|
-
record.recordTypeInfo = recordNode.object('recordTypeInfo').data;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// recurse on nested records
|
|
182
|
-
const fieldKeys = keys(record.fields);
|
|
183
|
-
const fieldKeysLen = fieldKeys.length;
|
|
184
|
-
for (let i = 0; i < fieldKeysLen; ++i) {
|
|
185
|
-
const fieldValue = record.fields[fieldKeys[i]].value;
|
|
186
|
-
if (isSpanningRecord(fieldValue)) {
|
|
187
|
-
fixRecordTypes(luvio, fieldValue);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Returns whether or not a the record is a DMO entity.
|
|
193
|
-
* @param record - The record.
|
|
194
|
-
* @returns True if DMO, false otherwise.
|
|
195
|
-
*/
|
|
196
|
-
function isDMOEntity(record) {
|
|
197
|
-
return record.apiName.endsWith(DMO_API_NAME_SUFFIX);
|
|
198
|
-
}
|
|
199
|
-
class AdsBridge {
|
|
200
|
-
constructor(luvio) {
|
|
201
|
-
this.luvio = luvio;
|
|
202
|
-
this.isRecordEmitLocked = false;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* This setter invoked by recordLibrary to listen for records ingested by Luvio. The passed method
|
|
206
|
-
* is invoked whenever a record is ingested. It may be via getRecord, getRecordUi, getListUi, ...
|
|
207
|
-
*/
|
|
208
|
-
set receiveFromLdsCallback(callback) {
|
|
209
|
-
// Unsubscribe if there is an existing subscription.
|
|
210
|
-
if (this.watchUnsubscribe !== undefined) {
|
|
211
|
-
this.watchUnsubscribe();
|
|
212
|
-
this.watchUnsubscribe = undefined;
|
|
213
|
-
}
|
|
214
|
-
if (callback !== undefined) {
|
|
215
|
-
this.watchUnsubscribe = this.luvio.storeWatch(RECORD_ID_PREFIX, (entries) => {
|
|
216
|
-
if (this.isRecordEmitLocked === true) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
this.emitRecordChanged(entries, callback);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* This method is invoked when a record has been ingested by ADS.
|
|
225
|
-
*
|
|
226
|
-
* ADS may invoke this method with records that are not UIAPI allowlisted so not refreshable by
|
|
227
|
-
* Luvio. Luvio filters the provided list so it ingests only UIAPI allowlisted records.
|
|
228
|
-
*/
|
|
229
|
-
addRecords(records, uiApiEntityAllowlist) {
|
|
230
|
-
const startTime = Date.now();
|
|
231
|
-
const { luvio } = this;
|
|
232
|
-
let didIngestRecord = false;
|
|
233
|
-
return this.lockLdsRecordEmit(() => {
|
|
234
|
-
for (let i = 0; i < records.length; i++) {
|
|
235
|
-
const record = records[i];
|
|
236
|
-
const { apiName } = record;
|
|
237
|
-
// Ingest the record if no allowlist is passed or the entity name is allowlisted.
|
|
238
|
-
if (uiApiEntityAllowlist === undefined ||
|
|
239
|
-
uiApiEntityAllowlist[apiName] !== 'false') {
|
|
240
|
-
didIngestRecord = true;
|
|
241
|
-
// Deep-copy the record to ingest and ingest the record copy. This avoids
|
|
242
|
-
// corrupting the ADS cache since ingestion mutates the passed record.
|
|
243
|
-
const recordCopy = parse(stringify(record));
|
|
244
|
-
// Don't let incorrect ADS/RecordGVP recordTypeIds replace a valid record type in our store
|
|
245
|
-
// with the master record type. See W-7302870 for details.
|
|
246
|
-
fixRecordTypes(luvio, recordCopy);
|
|
247
|
-
luvio.storeIngest(INGEST_KEY, ingestRecord, recordCopy);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (didIngestRecord === true) {
|
|
251
|
-
luvio.storeBroadcast();
|
|
252
|
-
}
|
|
253
|
-
instrumentation.timerMetricAddDuration(ADS_BRIDGE_ADD_RECORDS_DURATION, Date.now() - startTime);
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* This method is invoked whenever a record has been evicted from ADS.
|
|
258
|
-
*/
|
|
259
|
-
evict(recordId) {
|
|
260
|
-
const startTime = Date.now();
|
|
261
|
-
const { luvio } = this;
|
|
262
|
-
const key = keyBuilderRecord(luvio, { recordId });
|
|
263
|
-
return this.lockLdsRecordEmit(() => {
|
|
264
|
-
luvio.storeEvict(key);
|
|
265
|
-
luvio.storeBroadcast();
|
|
266
|
-
instrumentation.timerMetricAddDuration(ADS_BRIDGE_EVICT_DURATION, Date.now() - startTime);
|
|
267
|
-
return Promise.resolve();
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Gets the list of fields of a record that Luvio has in its store. The field list doesn't
|
|
272
|
-
* contains the spanning record fields. ADS uses this list when it loads a record from the
|
|
273
|
-
* server. This is an optimization to make a single roundtrip it queries for all fields required
|
|
274
|
-
* by ADS and Luvio.
|
|
275
|
-
*/
|
|
276
|
-
getTrackedFieldsForRecord(recordId) {
|
|
277
|
-
const { luvio } = this;
|
|
278
|
-
const storeRecordId = keyBuilderRecord(luvio, { recordId });
|
|
279
|
-
const recordNode = luvio.getNode(storeRecordId);
|
|
280
|
-
if (!isGraphNode(recordNode)) {
|
|
281
|
-
return Promise.resolve([]);
|
|
282
|
-
}
|
|
283
|
-
const apiName = recordNode.scalar('apiName');
|
|
284
|
-
const fieldNames = recordNode.object('fields').keys();
|
|
285
|
-
// Prefix all the fields with the record API name.
|
|
286
|
-
const qualifiedFieldNames = [];
|
|
287
|
-
for (let i = 0, len = fieldNames.length; i < len; i++) {
|
|
288
|
-
push.call(qualifiedFieldNames, `${apiName}.${fieldNames[i]}`);
|
|
289
|
-
}
|
|
290
|
-
return Promise.resolve(qualifiedFieldNames);
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Prevents the bridge to emit record change during the execution of the callback.
|
|
294
|
-
* This methods should wrap all the Luvio store mutation done by the bridge. It prevents Luvio store
|
|
295
|
-
* mutations triggered by ADS to be emit back to ADS.
|
|
296
|
-
*/
|
|
297
|
-
lockLdsRecordEmit(callback) {
|
|
298
|
-
this.isRecordEmitLocked = true;
|
|
299
|
-
try {
|
|
300
|
-
return callback();
|
|
301
|
-
}
|
|
302
|
-
finally {
|
|
303
|
-
this.isRecordEmitLocked = false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* This method retrieves queries the store with with passed record ids to retrieve their
|
|
308
|
-
* associated records and object info. Note that the passed ids are not Salesforce record id
|
|
309
|
-
* but rather Luvio internals store ids.
|
|
310
|
-
*/
|
|
311
|
-
emitRecordChanged(updatedEntries, callback) {
|
|
312
|
-
const startTime = Date.now();
|
|
313
|
-
const { luvio } = this;
|
|
314
|
-
let shouldEmit = false;
|
|
315
|
-
const adsRecordMap = {};
|
|
316
|
-
const adsObjectMap = {};
|
|
317
|
-
for (let i = 0; i < updatedEntries.length; i++) {
|
|
318
|
-
const storeRecordId = updatedEntries[i].id;
|
|
319
|
-
// Exclude all the store record ids not matching with the record id pattern.
|
|
320
|
-
// Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
|
|
321
|
-
// need to filter them out.
|
|
322
|
-
if (!isStoreKeyRecordId(storeRecordId)) {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const record = getShallowRecord(luvio, storeRecordId);
|
|
326
|
-
if (record === null) {
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
// W-9978523
|
|
330
|
-
if (isDMOEntity(record) === true) {
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const { id, apiName } = record;
|
|
334
|
-
shouldEmit = true;
|
|
335
|
-
adsRecordMap[id] = {
|
|
336
|
-
[apiName]: {
|
|
337
|
-
isPrimary: true,
|
|
338
|
-
record,
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
// Extract and add the object metadata to the map if not already present.
|
|
342
|
-
if (!hasOwnProperty.call(adsObjectMap, apiName)) {
|
|
343
|
-
adsObjectMap[apiName] = getObjectMetadata(luvio, record);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
if (shouldEmit === true) {
|
|
347
|
-
callback(adsRecordMap, adsObjectMap);
|
|
348
|
-
}
|
|
349
|
-
instrumentation.timerMetricAddDuration(ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION, Date.now() - startTime);
|
|
350
|
-
}
|
|
57
|
+
// No need to pass the actual record key `luvio.ingestStore`. The `RecordRepresentation.ts#ingest`
|
|
58
|
+
// function extracts the appropriate record id from the ingested record.
|
|
59
|
+
const INGEST_KEY = '';
|
|
60
|
+
const MASTER_RECORD_TYPE_ID = '012000000000000AAA';
|
|
61
|
+
const DMO_API_NAME_SUFFIX = '__dlm';
|
|
62
|
+
function isGraphNode(node) {
|
|
63
|
+
return node !== null && node.type === 'Node';
|
|
64
|
+
}
|
|
65
|
+
function isSpanningRecord(fieldValue) {
|
|
66
|
+
return fieldValue !== null && typeof fieldValue === 'object';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns a shallow copy of a record with its field values if it is a scalar and a reference and a
|
|
70
|
+
* a RecordRepresentation with no field if the value if a spanning record.
|
|
71
|
+
* It returns null if the record contains any pending field.
|
|
72
|
+
*/
|
|
73
|
+
function getShallowRecord(luvio, storeRecordId) {
|
|
74
|
+
const recordNode = luvio.getNode(storeRecordId);
|
|
75
|
+
if (!isGraphNode(recordNode)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const fieldsCopy = {};
|
|
79
|
+
const copy = {
|
|
80
|
+
...recordNode.retrieve(),
|
|
81
|
+
fields: fieldsCopy,
|
|
82
|
+
childRelationships: {},
|
|
83
|
+
};
|
|
84
|
+
const fieldsNode = recordNode.object('fields');
|
|
85
|
+
const fieldNames = fieldsNode.keys();
|
|
86
|
+
for (let i = 0, len = fieldNames.length; i < len; i++) {
|
|
87
|
+
let fieldCopy;
|
|
88
|
+
const fieldName = fieldNames[i];
|
|
89
|
+
const fieldLink = fieldsNode.link(fieldName);
|
|
90
|
+
if (fieldLink.isPending() === true) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const fieldNode = fieldLink.follow();
|
|
94
|
+
if (!isGraphNode(fieldNode)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const { displayValue, value } = fieldNode.retrieve();
|
|
98
|
+
if (fieldNode.isScalar('value')) {
|
|
99
|
+
fieldCopy = {
|
|
100
|
+
displayValue: displayValue,
|
|
101
|
+
value: value,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const spanningRecordLink = fieldNode.link('value');
|
|
106
|
+
if (spanningRecordLink.isPending() === true) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const spanningRecordNode = spanningRecordLink.follow();
|
|
110
|
+
if (!isGraphNode(spanningRecordNode)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
fieldCopy = {
|
|
114
|
+
displayValue,
|
|
115
|
+
value: {
|
|
116
|
+
...spanningRecordNode.retrieve(),
|
|
117
|
+
fields: {},
|
|
118
|
+
childRelationships: {},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
fieldsCopy[fieldName] = fieldCopy;
|
|
123
|
+
}
|
|
124
|
+
return copy;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Returns the ADS object metadata representation for a specific record.
|
|
128
|
+
*/
|
|
129
|
+
function getObjectMetadata(luvio, record) {
|
|
130
|
+
const { data: objectInfo } = luvio.storeLookup({
|
|
131
|
+
recordId: keyBuilderObjectInfo(luvio, { apiName: record.apiName }),
|
|
132
|
+
node: {
|
|
133
|
+
kind: 'Fragment',
|
|
134
|
+
private: ['eTag'],
|
|
135
|
+
opaque: true,
|
|
136
|
+
},
|
|
137
|
+
variables: {},
|
|
138
|
+
});
|
|
139
|
+
if (objectInfo !== undefined) {
|
|
140
|
+
let nameField = 'Name';
|
|
141
|
+
// Extract the entity name field from the object info. In the case where there are multiple
|
|
142
|
+
// field names then pick up the first one.
|
|
143
|
+
if (objectInfo.nameFields.length !== 0 && objectInfo.nameFields.indexOf('Name') === -1) {
|
|
144
|
+
nameField = objectInfo.nameFields[0];
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
_nameField: nameField,
|
|
148
|
+
_entityLabel: objectInfo.label,
|
|
149
|
+
_keyPrefix: objectInfo.keyPrefix,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
_nameField: 'Name',
|
|
154
|
+
_entityLabel: record.apiName,
|
|
155
|
+
_keyPrefix: record.id.substring(0, 3),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* RecordGvp can send records back to ADS with record types incorrectly set to the master
|
|
160
|
+
* record type. Since there are no known legitimate scenarios where a record can change from a
|
|
161
|
+
* non-master record type back to the master record type, we assume such a transition
|
|
162
|
+
* indicates a RecordGvp mistake. This function checks for that scenario and overwrites the
|
|
163
|
+
* incoming ADS record type information with what we already have in the store when it
|
|
164
|
+
* occurs.
|
|
165
|
+
*
|
|
166
|
+
* @param luvio Luvio
|
|
167
|
+
* @param record record from ADS, will be fixed in situ
|
|
168
|
+
*/
|
|
169
|
+
function fixRecordTypes(luvio, record) {
|
|
170
|
+
// non-master record types should always be correct
|
|
171
|
+
if (record.recordTypeId === MASTER_RECORD_TYPE_ID) {
|
|
172
|
+
const key = keyBuilderRecord(luvio, { recordId: record.id });
|
|
173
|
+
const recordNode = luvio.getNode(key);
|
|
174
|
+
if (isGraphNode(recordNode) &&
|
|
175
|
+
recordNode.scalar('recordTypeId') !== MASTER_RECORD_TYPE_ID) {
|
|
176
|
+
// ignore bogus incoming record type information & keep what we have
|
|
177
|
+
record.recordTypeId = recordNode.scalar('recordTypeId');
|
|
178
|
+
record.recordTypeInfo = recordNode.object('recordTypeInfo').data;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// recurse on nested records
|
|
182
|
+
const fieldKeys = keys(record.fields);
|
|
183
|
+
const fieldKeysLen = fieldKeys.length;
|
|
184
|
+
for (let i = 0; i < fieldKeysLen; ++i) {
|
|
185
|
+
const fieldValue = record.fields[fieldKeys[i]].value;
|
|
186
|
+
if (isSpanningRecord(fieldValue)) {
|
|
187
|
+
fixRecordTypes(luvio, fieldValue);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Returns whether or not a the record is a DMO entity.
|
|
193
|
+
* @param record - The record.
|
|
194
|
+
* @returns True if DMO, false otherwise.
|
|
195
|
+
*/
|
|
196
|
+
function isDMOEntity(record) {
|
|
197
|
+
return record.apiName.endsWith(DMO_API_NAME_SUFFIX);
|
|
198
|
+
}
|
|
199
|
+
class AdsBridge {
|
|
200
|
+
constructor(luvio) {
|
|
201
|
+
this.luvio = luvio;
|
|
202
|
+
this.isRecordEmitLocked = false;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* This setter invoked by recordLibrary to listen for records ingested by Luvio. The passed method
|
|
206
|
+
* is invoked whenever a record is ingested. It may be via getRecord, getRecordUi, getListUi, ...
|
|
207
|
+
*/
|
|
208
|
+
set receiveFromLdsCallback(callback) {
|
|
209
|
+
// Unsubscribe if there is an existing subscription.
|
|
210
|
+
if (this.watchUnsubscribe !== undefined) {
|
|
211
|
+
this.watchUnsubscribe();
|
|
212
|
+
this.watchUnsubscribe = undefined;
|
|
213
|
+
}
|
|
214
|
+
if (callback !== undefined) {
|
|
215
|
+
this.watchUnsubscribe = this.luvio.storeWatch(RECORD_ID_PREFIX, (entries) => {
|
|
216
|
+
if (this.isRecordEmitLocked === true) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.emitRecordChanged(entries, callback);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* This method is invoked when a record has been ingested by ADS.
|
|
225
|
+
*
|
|
226
|
+
* ADS may invoke this method with records that are not UIAPI allowlisted so not refreshable by
|
|
227
|
+
* Luvio. Luvio filters the provided list so it ingests only UIAPI allowlisted records.
|
|
228
|
+
*/
|
|
229
|
+
addRecords(records, uiApiEntityAllowlist) {
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
const { luvio } = this;
|
|
232
|
+
let didIngestRecord = false;
|
|
233
|
+
return this.lockLdsRecordEmit(() => {
|
|
234
|
+
for (let i = 0; i < records.length; i++) {
|
|
235
|
+
const record = records[i];
|
|
236
|
+
const { apiName } = record;
|
|
237
|
+
// Ingest the record if no allowlist is passed or the entity name is allowlisted.
|
|
238
|
+
if (uiApiEntityAllowlist === undefined ||
|
|
239
|
+
uiApiEntityAllowlist[apiName] !== 'false') {
|
|
240
|
+
didIngestRecord = true;
|
|
241
|
+
// Deep-copy the record to ingest and ingest the record copy. This avoids
|
|
242
|
+
// corrupting the ADS cache since ingestion mutates the passed record.
|
|
243
|
+
const recordCopy = parse(stringify(record));
|
|
244
|
+
// Don't let incorrect ADS/RecordGVP recordTypeIds replace a valid record type in our store
|
|
245
|
+
// with the master record type. See W-7302870 for details.
|
|
246
|
+
fixRecordTypes(luvio, recordCopy);
|
|
247
|
+
luvio.storeIngest(INGEST_KEY, ingestRecord, recordCopy);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (didIngestRecord === true) {
|
|
251
|
+
luvio.storeBroadcast();
|
|
252
|
+
}
|
|
253
|
+
instrumentation.timerMetricAddDuration(ADS_BRIDGE_ADD_RECORDS_DURATION, Date.now() - startTime);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* This method is invoked whenever a record has been evicted from ADS.
|
|
258
|
+
*/
|
|
259
|
+
evict(recordId) {
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
const { luvio } = this;
|
|
262
|
+
const key = keyBuilderRecord(luvio, { recordId });
|
|
263
|
+
return this.lockLdsRecordEmit(() => {
|
|
264
|
+
luvio.storeEvict(key);
|
|
265
|
+
luvio.storeBroadcast();
|
|
266
|
+
instrumentation.timerMetricAddDuration(ADS_BRIDGE_EVICT_DURATION, Date.now() - startTime);
|
|
267
|
+
return Promise.resolve();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Gets the list of fields of a record that Luvio has in its store. The field list doesn't
|
|
272
|
+
* contains the spanning record fields. ADS uses this list when it loads a record from the
|
|
273
|
+
* server. This is an optimization to make a single roundtrip it queries for all fields required
|
|
274
|
+
* by ADS and Luvio.
|
|
275
|
+
*/
|
|
276
|
+
getTrackedFieldsForRecord(recordId) {
|
|
277
|
+
const { luvio } = this;
|
|
278
|
+
const storeRecordId = keyBuilderRecord(luvio, { recordId });
|
|
279
|
+
const recordNode = luvio.getNode(storeRecordId);
|
|
280
|
+
if (!isGraphNode(recordNode)) {
|
|
281
|
+
return Promise.resolve([]);
|
|
282
|
+
}
|
|
283
|
+
const apiName = recordNode.scalar('apiName');
|
|
284
|
+
const fieldNames = recordNode.object('fields').keys();
|
|
285
|
+
// Prefix all the fields with the record API name.
|
|
286
|
+
const qualifiedFieldNames = [];
|
|
287
|
+
for (let i = 0, len = fieldNames.length; i < len; i++) {
|
|
288
|
+
push.call(qualifiedFieldNames, `${apiName}.${fieldNames[i]}`);
|
|
289
|
+
}
|
|
290
|
+
return Promise.resolve(qualifiedFieldNames);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Prevents the bridge to emit record change during the execution of the callback.
|
|
294
|
+
* This methods should wrap all the Luvio store mutation done by the bridge. It prevents Luvio store
|
|
295
|
+
* mutations triggered by ADS to be emit back to ADS.
|
|
296
|
+
*/
|
|
297
|
+
lockLdsRecordEmit(callback) {
|
|
298
|
+
this.isRecordEmitLocked = true;
|
|
299
|
+
try {
|
|
300
|
+
return callback();
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
this.isRecordEmitLocked = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* This method retrieves queries the store with with passed record ids to retrieve their
|
|
308
|
+
* associated records and object info. Note that the passed ids are not Salesforce record id
|
|
309
|
+
* but rather Luvio internals store ids.
|
|
310
|
+
*/
|
|
311
|
+
emitRecordChanged(updatedEntries, callback) {
|
|
312
|
+
const startTime = Date.now();
|
|
313
|
+
const { luvio } = this;
|
|
314
|
+
let shouldEmit = false;
|
|
315
|
+
const adsRecordMap = {};
|
|
316
|
+
const adsObjectMap = {};
|
|
317
|
+
for (let i = 0; i < updatedEntries.length; i++) {
|
|
318
|
+
const storeRecordId = updatedEntries[i].id;
|
|
319
|
+
// Exclude all the store record ids not matching with the record id pattern.
|
|
320
|
+
// Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
|
|
321
|
+
// need to filter them out.
|
|
322
|
+
if (!isStoreKeyRecordId(storeRecordId)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const record = getShallowRecord(luvio, storeRecordId);
|
|
326
|
+
if (record === null) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// W-9978523
|
|
330
|
+
if (isDMOEntity(record) === true) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const { id, apiName } = record;
|
|
334
|
+
shouldEmit = true;
|
|
335
|
+
adsRecordMap[id] = {
|
|
336
|
+
[apiName]: {
|
|
337
|
+
isPrimary: true,
|
|
338
|
+
record,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
// Extract and add the object metadata to the map if not already present.
|
|
342
|
+
if (!hasOwnProperty.call(adsObjectMap, apiName)) {
|
|
343
|
+
adsObjectMap[apiName] = getObjectMetadata(luvio, record);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (shouldEmit === true) {
|
|
347
|
+
callback(adsRecordMap, adsObjectMap);
|
|
348
|
+
}
|
|
349
|
+
instrumentation.timerMetricAddDuration(ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION, Date.now() - startTime);
|
|
350
|
+
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
// most recently created AdsBridge
|
|
354
|
-
let adsBridge;
|
|
355
|
-
// callbacks to be invoked when AdsBridge is set/changed
|
|
356
|
-
let callbacks = [];
|
|
357
|
-
// create a new AdsBridge whenever the default Luvio is set/changed
|
|
358
|
-
withDefaultLuvio((luvio) => {
|
|
359
|
-
adsBridge = new AdsBridge(luvio);
|
|
360
|
-
for (let i = 0; i < callbacks.length; ++i) {
|
|
361
|
-
callbacks[i](adsBridge);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
/**
|
|
365
|
-
* Registers a callback to be invoked with the AdsBridge instance. Note that the
|
|
366
|
-
* callback may be invoked multiple times if the default Luvio changes.
|
|
367
|
-
*
|
|
368
|
-
* @param callback callback to be invoked with the AdsBridge
|
|
369
|
-
*/
|
|
370
|
-
function withAdsBridge(callback) {
|
|
371
|
-
if (adsBridge) {
|
|
372
|
-
callback(adsBridge);
|
|
373
|
-
}
|
|
374
|
-
callbacks.push(callback);
|
|
353
|
+
// most recently created AdsBridge
|
|
354
|
+
let adsBridge;
|
|
355
|
+
// callbacks to be invoked when AdsBridge is set/changed
|
|
356
|
+
let callbacks = [];
|
|
357
|
+
// create a new AdsBridge whenever the default Luvio is set/changed
|
|
358
|
+
withDefaultLuvio((luvio) => {
|
|
359
|
+
adsBridge = new AdsBridge(luvio);
|
|
360
|
+
for (let i = 0; i < callbacks.length; ++i) {
|
|
361
|
+
callbacks[i](adsBridge);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
/**
|
|
365
|
+
* Registers a callback to be invoked with the AdsBridge instance. Note that the
|
|
366
|
+
* callback may be invoked multiple times if the default Luvio changes.
|
|
367
|
+
*
|
|
368
|
+
* @param callback callback to be invoked with the AdsBridge
|
|
369
|
+
*/
|
|
370
|
+
function withAdsBridge(callback) {
|
|
371
|
+
if (adsBridge) {
|
|
372
|
+
callback(adsBridge);
|
|
373
|
+
}
|
|
374
|
+
callbacks.push(callback);
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
export { instrument, withAdsBridge };
|
|
378
|
-
// version: 1.124.
|
|
378
|
+
// version: 1.124.3-cf2dbb2fa
|