@salesforce/lds-network-nimbus 1.124.2 → 1.124.4
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/main.js +752 -752
- package/dist/{main.d.ts → types/main.d.ts} +3 -3
- package/dist/{network → types/network}/NimbusNetworkAdapter.d.ts +2 -2
- package/dist/{network → types/network}/__mocks__/o11y/activity.d.ts +2 -2
- package/dist/{network → types/network}/__mocks__/o11y/client.d.ts +5 -5
- package/dist/{network → types/network}/__mocks__/o11y/idleDetector.d.ts +2 -2
- package/dist/{network → types/network}/__mocks__/o11y/instrumentation.d.ts +15 -15
- package/dist/{network → types/network}/networkUtils.d.ts +4 -4
- package/dist/{network → types/network}/record-field-batching/ScopedFields.d.ts +44 -44
- package/dist/{network → types/network}/record-field-batching/makeNetworkAdapterChunkRecordFields.d.ts +9 -9
- package/dist/{network → types/network}/record-field-batching/makeNetworkChunkFieldsGetRecord.d.ts +15 -15
- package/dist/{network → types/network}/record-field-batching/makeNetworkChunkFieldsGetRecordsBatch.d.ts +7 -7
- package/dist/{network → types/network}/record-field-batching/makeNetworkChunkFieldsGetRelatedListRecords.d.ts +22 -22
- package/dist/{network → types/network}/record-field-batching/makeNetworkChunkFieldsGetRelatedListRecordsBatch.d.ts +6 -6
- package/dist/{network → types/network}/record-field-batching/utils.d.ts +71 -71
- package/dist/{utils → types/utils}/language.d.ts +29 -29
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -7,776 +7,776 @@
|
|
|
7
7
|
import { idleDetector } from 'o11y/client';
|
|
8
8
|
import { HttpStatusCode } from '@luvio/engine';
|
|
9
9
|
|
|
10
|
-
const { keys, create, assign, entries } = Object;
|
|
11
|
-
const { stringify, parse } = JSON;
|
|
12
|
-
const { push, join, slice } = Array.prototype;
|
|
10
|
+
const { keys, create, assign, entries } = Object;
|
|
11
|
+
const { stringify, parse } = JSON;
|
|
12
|
+
const { push, join, slice } = Array.prototype;
|
|
13
13
|
const { isArray, from } = Array;
|
|
14
14
|
|
|
15
|
-
function ldsParamsToString(params) {
|
|
16
|
-
const returnParams = create(null);
|
|
17
|
-
const keys$1 = keys(params);
|
|
18
|
-
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
19
|
-
const key = keys$1[i];
|
|
20
|
-
const value = params[key];
|
|
21
|
-
if (value === undefined) {
|
|
22
|
-
// filter out params that have no value
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
if (isArray(value)) {
|
|
26
|
-
// filter out empty arrays
|
|
27
|
-
if (value.length > 0) {
|
|
28
|
-
returnParams[key] = value.join(',');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
returnParams[key] = `${value}`;
|
|
33
|
-
}
|
|
34
|
-
if (isObject(value) === true && keys(value).length > 0) {
|
|
35
|
-
returnParams[key] = stringify(value);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return returnParams;
|
|
39
|
-
}
|
|
40
|
-
function statusTextFromStatusCode(status) {
|
|
41
|
-
switch (status) {
|
|
42
|
-
case HttpStatusCode.Ok:
|
|
43
|
-
return 'OK';
|
|
44
|
-
case HttpStatusCode.NotModified:
|
|
45
|
-
return 'Not Modified';
|
|
46
|
-
case HttpStatusCode.NotFound:
|
|
47
|
-
return 'Not Found';
|
|
48
|
-
case HttpStatusCode.BadRequest:
|
|
49
|
-
return 'Bad Request';
|
|
50
|
-
case HttpStatusCode.ServerError:
|
|
51
|
-
return 'Server Error';
|
|
52
|
-
default:
|
|
53
|
-
return `Unexpected HTTP Status Code: ${status}`;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function methodFromResourceRequestMethod(method) {
|
|
57
|
-
switch (method.toLowerCase()) {
|
|
58
|
-
case 'get':
|
|
59
|
-
return 'GET';
|
|
60
|
-
case 'put':
|
|
61
|
-
return 'PUT';
|
|
62
|
-
case 'post':
|
|
63
|
-
return 'POST';
|
|
64
|
-
case 'delete':
|
|
65
|
-
return 'DELETE';
|
|
66
|
-
case 'patch':
|
|
67
|
-
return 'PATCH';
|
|
68
|
-
default:
|
|
69
|
-
throw Error(`Unexpected method ${method}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function priorityFromResourceRequest(request) {
|
|
73
|
-
switch (request.priority) {
|
|
74
|
-
case 'background':
|
|
75
|
-
return 'background';
|
|
76
|
-
case 'high':
|
|
77
|
-
return 'high';
|
|
78
|
-
case 'normal':
|
|
79
|
-
default:
|
|
80
|
-
return 'normal';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function isStatusOk(status) {
|
|
84
|
-
return status >= 200 && status <= 299;
|
|
85
|
-
}
|
|
86
|
-
// adapted from adapter-utils untrustedIsObject
|
|
87
|
-
function isObject(value) {
|
|
88
|
-
return typeof value === 'object' && value !== null && isArray(value) === false;
|
|
89
|
-
}
|
|
90
|
-
function stringifyIfPresent(value) {
|
|
91
|
-
if (value === undefined || value === null) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
return stringify(value);
|
|
95
|
-
}
|
|
96
|
-
function parseIfPresent(value) {
|
|
97
|
-
if (value === undefined || value === null || value === '') {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
return parse(value);
|
|
101
|
-
}
|
|
102
|
-
function buildNimbusNetworkPluginRequest(resourceRequest, resourceRequestContext) {
|
|
103
|
-
const { basePath, baseUri, method, headers, queryParams, body } = resourceRequest;
|
|
104
|
-
let observabilityContext = null;
|
|
105
|
-
if (resourceRequestContext !== undefined &&
|
|
106
|
-
resourceRequestContext.requestCorrelator !== undefined &&
|
|
107
|
-
resourceRequestContext.requestCorrelator.observabilityContext !==
|
|
108
|
-
undefined) {
|
|
109
|
-
({ observabilityContext = null } =
|
|
110
|
-
resourceRequestContext.requestCorrelator);
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
method: methodFromResourceRequestMethod(method),
|
|
114
|
-
body: stringifyIfPresent(body),
|
|
115
|
-
headers,
|
|
116
|
-
queryParams: ldsParamsToString(queryParams),
|
|
117
|
-
path: `${baseUri}${basePath}`,
|
|
118
|
-
priority: priorityFromResourceRequest(resourceRequest),
|
|
119
|
-
observabilityContext,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
function buildLdsResponse(response) {
|
|
123
|
-
const { body: responseBody, headers, status } = response;
|
|
124
|
-
const statusText = statusTextFromStatusCode(status);
|
|
125
|
-
return {
|
|
126
|
-
statusText,
|
|
127
|
-
status,
|
|
128
|
-
body: parseIfPresent(responseBody),
|
|
129
|
-
headers,
|
|
130
|
-
ok: isStatusOk(status),
|
|
131
|
-
};
|
|
15
|
+
function ldsParamsToString(params) {
|
|
16
|
+
const returnParams = create(null);
|
|
17
|
+
const keys$1 = keys(params);
|
|
18
|
+
for (let i = 0, len = keys$1.length; i < len; i++) {
|
|
19
|
+
const key = keys$1[i];
|
|
20
|
+
const value = params[key];
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
// filter out params that have no value
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (isArray(value)) {
|
|
26
|
+
// filter out empty arrays
|
|
27
|
+
if (value.length > 0) {
|
|
28
|
+
returnParams[key] = value.join(',');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
returnParams[key] = `${value}`;
|
|
33
|
+
}
|
|
34
|
+
if (isObject(value) === true && keys(value).length > 0) {
|
|
35
|
+
returnParams[key] = stringify(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return returnParams;
|
|
39
|
+
}
|
|
40
|
+
function statusTextFromStatusCode(status) {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case HttpStatusCode.Ok:
|
|
43
|
+
return 'OK';
|
|
44
|
+
case HttpStatusCode.NotModified:
|
|
45
|
+
return 'Not Modified';
|
|
46
|
+
case HttpStatusCode.NotFound:
|
|
47
|
+
return 'Not Found';
|
|
48
|
+
case HttpStatusCode.BadRequest:
|
|
49
|
+
return 'Bad Request';
|
|
50
|
+
case HttpStatusCode.ServerError:
|
|
51
|
+
return 'Server Error';
|
|
52
|
+
default:
|
|
53
|
+
return `Unexpected HTTP Status Code: ${status}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function methodFromResourceRequestMethod(method) {
|
|
57
|
+
switch (method.toLowerCase()) {
|
|
58
|
+
case 'get':
|
|
59
|
+
return 'GET';
|
|
60
|
+
case 'put':
|
|
61
|
+
return 'PUT';
|
|
62
|
+
case 'post':
|
|
63
|
+
return 'POST';
|
|
64
|
+
case 'delete':
|
|
65
|
+
return 'DELETE';
|
|
66
|
+
case 'patch':
|
|
67
|
+
return 'PATCH';
|
|
68
|
+
default:
|
|
69
|
+
throw Error(`Unexpected method ${method}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function priorityFromResourceRequest(request) {
|
|
73
|
+
switch (request.priority) {
|
|
74
|
+
case 'background':
|
|
75
|
+
return 'background';
|
|
76
|
+
case 'high':
|
|
77
|
+
return 'high';
|
|
78
|
+
case 'normal':
|
|
79
|
+
default:
|
|
80
|
+
return 'normal';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isStatusOk(status) {
|
|
84
|
+
return status >= 200 && status <= 299;
|
|
85
|
+
}
|
|
86
|
+
// adapted from adapter-utils untrustedIsObject
|
|
87
|
+
function isObject(value) {
|
|
88
|
+
return typeof value === 'object' && value !== null && isArray(value) === false;
|
|
89
|
+
}
|
|
90
|
+
function stringifyIfPresent(value) {
|
|
91
|
+
if (value === undefined || value === null) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return stringify(value);
|
|
95
|
+
}
|
|
96
|
+
function parseIfPresent(value) {
|
|
97
|
+
if (value === undefined || value === null || value === '') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return parse(value);
|
|
101
|
+
}
|
|
102
|
+
function buildNimbusNetworkPluginRequest(resourceRequest, resourceRequestContext) {
|
|
103
|
+
const { basePath, baseUri, method, headers, queryParams, body } = resourceRequest;
|
|
104
|
+
let observabilityContext = null;
|
|
105
|
+
if (resourceRequestContext !== undefined &&
|
|
106
|
+
resourceRequestContext.requestCorrelator !== undefined &&
|
|
107
|
+
resourceRequestContext.requestCorrelator.observabilityContext !==
|
|
108
|
+
undefined) {
|
|
109
|
+
({ observabilityContext = null } =
|
|
110
|
+
resourceRequestContext.requestCorrelator);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
method: methodFromResourceRequestMethod(method),
|
|
114
|
+
body: stringifyIfPresent(body),
|
|
115
|
+
headers,
|
|
116
|
+
queryParams: ldsParamsToString(queryParams),
|
|
117
|
+
path: `${baseUri}${basePath}`,
|
|
118
|
+
priority: priorityFromResourceRequest(resourceRequest),
|
|
119
|
+
observabilityContext,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function buildLdsResponse(response) {
|
|
123
|
+
const { body: responseBody, headers, status } = response;
|
|
124
|
+
const statusText = statusTextFromStatusCode(status);
|
|
125
|
+
return {
|
|
126
|
+
statusText,
|
|
127
|
+
status,
|
|
128
|
+
body: parseIfPresent(responseBody),
|
|
129
|
+
headers,
|
|
130
|
+
ok: isStatusOk(status),
|
|
131
|
+
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// so eslint doesn't complain about nimbus
|
|
135
|
-
const tasker = idleDetector.declareNotifierTaskMulti('NimbusNetworkAdapter');
|
|
136
|
-
const NimbusNetworkAdapter = (request, resourceRequestContext) => {
|
|
137
|
-
tasker.add();
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
try {
|
|
140
|
-
__nimbus.plugins.LdsNetworkAdapter.sendRequest(buildNimbusNetworkPluginRequest(request, resourceRequestContext), (response) => {
|
|
141
|
-
try {
|
|
142
|
-
resolve(buildLdsResponse(response));
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
// don't leave promise hanging, catch any errors (eg: if native side
|
|
146
|
-
// returns malformed response) and call reject
|
|
147
|
-
reject(error);
|
|
148
|
-
}
|
|
149
|
-
}, (error) => {
|
|
150
|
-
reject(new Error(`type: ${error.type}, message: ${error.message}`));
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
// don't leave promise hanging, catch any errors (eg: if native side
|
|
155
|
-
// fails to parse the request), and call reject
|
|
156
|
-
reject(error);
|
|
157
|
-
}
|
|
158
|
-
}).finally(() => tasker.done());
|
|
134
|
+
// so eslint doesn't complain about nimbus
|
|
135
|
+
const tasker = idleDetector.declareNotifierTaskMulti('NimbusNetworkAdapter');
|
|
136
|
+
const NimbusNetworkAdapter = (request, resourceRequestContext) => {
|
|
137
|
+
tasker.add();
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
try {
|
|
140
|
+
__nimbus.plugins.LdsNetworkAdapter.sendRequest(buildNimbusNetworkPluginRequest(request, resourceRequestContext), (response) => {
|
|
141
|
+
try {
|
|
142
|
+
resolve(buildLdsResponse(response));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// don't leave promise hanging, catch any errors (eg: if native side
|
|
146
|
+
// returns malformed response) and call reject
|
|
147
|
+
reject(error);
|
|
148
|
+
}
|
|
149
|
+
}, (error) => {
|
|
150
|
+
reject(new Error(`type: ${error.type}, message: ${error.message}`));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// don't leave promise hanging, catch any errors (eg: if native side
|
|
155
|
+
// fails to parse the request), and call reject
|
|
156
|
+
reject(error);
|
|
157
|
+
}
|
|
158
|
+
}).finally(() => tasker.done());
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
* related-list-records/batch fields and optional fields could be like below.
|
|
163
|
-
* /ui-api/related-list-records/batch/001R0000006l1xKIAQ/Contacts,Opportunities?
|
|
164
|
-
* optionalFields=Contacts:Contact.Id,Contact.Name;Opportunities:Opportunity.Id
|
|
165
|
-
*
|
|
166
|
-
* the pattern is [{relativeListId}:{[fields]};{relativeListId}:{[fields]}]
|
|
167
|
-
*/
|
|
168
|
-
const SEPARATOR_BETWEEN_SCOPES = ';';
|
|
169
|
-
const SEPARATOR_BETWEEN_SCOPE_AND_FIELDS = ':';
|
|
170
|
-
const SEPARATOR_BETWEEN_FIELDS = ',';
|
|
171
|
-
const UNSCOPED_IDENTIFIER = 'unscoped';
|
|
172
|
-
class ScopedFields {
|
|
173
|
-
constructor(scope, fields) {
|
|
174
|
-
this.fields = {};
|
|
175
|
-
this.scope = scope;
|
|
176
|
-
for (let i = 0, len = fields.length; i < len; i += 1) {
|
|
177
|
-
this.fields[fields[i]] = true;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
isUnScoped() {
|
|
181
|
-
return this.scope === UNSCOPED_IDENTIFIER;
|
|
182
|
-
}
|
|
183
|
-
addField(field) {
|
|
184
|
-
this.fields[field] = true;
|
|
185
|
-
}
|
|
186
|
-
addFields(fields) {
|
|
187
|
-
fields.forEach(this.addField, this);
|
|
188
|
-
}
|
|
189
|
-
toQueryParameterValue() {
|
|
190
|
-
const joinedFields = join.call(Object.keys(this.fields), SEPARATOR_BETWEEN_FIELDS);
|
|
191
|
-
return this.isUnScoped()
|
|
192
|
-
? joinedFields
|
|
193
|
-
: join.call([this.scope, joinedFields], SEPARATOR_BETWEEN_SCOPE_AND_FIELDS);
|
|
194
|
-
}
|
|
195
|
-
toQueryParams() {
|
|
196
|
-
return this.isUnScoped() ? Object.keys(this.fields) : this.toQueryParameterValue();
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* parse Contacts:Contact.Id,Contact.Name into a QueryFields
|
|
200
|
-
*/
|
|
201
|
-
static fromQueryParameterValue(paramValue) {
|
|
202
|
-
if (paramValue === null || paramValue === '')
|
|
203
|
-
return;
|
|
204
|
-
const scopeToFields = paramValue.split(SEPARATOR_BETWEEN_SCOPE_AND_FIELDS);
|
|
205
|
-
if (scopeToFields.length === 1) {
|
|
206
|
-
//unscoped
|
|
207
|
-
return new ScopedFields(UNSCOPED_IDENTIFIER, scopeToFields[0].split(SEPARATOR_BETWEEN_FIELDS));
|
|
208
|
-
}
|
|
209
|
-
else if (scopeToFields.length === 2) {
|
|
210
|
-
const scope = scopeToFields[0];
|
|
211
|
-
const fields = scopeToFields[1];
|
|
212
|
-
if (scope === undefined || fields === null)
|
|
213
|
-
return;
|
|
214
|
-
return new ScopedFields(scope, fields.split(SEPARATOR_BETWEEN_FIELDS));
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
class ScopedFieldsCollection {
|
|
222
|
-
constructor() {
|
|
223
|
-
this.listIdToFieldsMap = {};
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* merge the from ScopedFieldsCollection into current one
|
|
227
|
-
* @param from
|
|
228
|
-
*/
|
|
229
|
-
merge(from) {
|
|
230
|
-
const { listIdToFieldsMap } = from;
|
|
231
|
-
const scopes = Object.keys(listIdToFieldsMap);
|
|
232
|
-
for (let i = 0, len = scopes.length; i < len; i += 1) {
|
|
233
|
-
const scope = scopes[i];
|
|
234
|
-
const scopedFields = listIdToFieldsMap[scope];
|
|
235
|
-
const existingScopedFields = this.listIdToFieldsMap[scope];
|
|
236
|
-
if (existingScopedFields) {
|
|
237
|
-
existingScopedFields.addFields(Object.keys(scopedFields.fields));
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
this.listIdToFieldsMap[scope] = scopedFields;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
toQueryParams() {
|
|
245
|
-
return this.countOfUnScoped() > 0
|
|
246
|
-
? this.listIdToFieldsMap[UNSCOPED_IDENTIFIER].toQueryParams()
|
|
247
|
-
: this.toQueryParameterValue();
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* convert to query parameter value
|
|
251
|
-
* @returns
|
|
252
|
-
*/
|
|
253
|
-
toQueryParameterValue() {
|
|
254
|
-
let result = [];
|
|
255
|
-
const scopes = Object.keys(this.listIdToFieldsMap);
|
|
256
|
-
for (let i = 0, len = scopes.length; i < len; i += 1) {
|
|
257
|
-
const chunk = this.listIdToFieldsMap[scopes[i]].toQueryParameterValue();
|
|
258
|
-
if (chunk !== undefined)
|
|
259
|
-
result.push(chunk);
|
|
260
|
-
}
|
|
261
|
-
return join.call(result, SEPARATOR_BETWEEN_SCOPES);
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* split the ScopedFields into multiple ScopedFields
|
|
265
|
-
* which there max fields list length will not exceeded the specified maxLength
|
|
266
|
-
* @param maxLength
|
|
267
|
-
*/
|
|
268
|
-
split(maxLength = MAX_STRING_LENGTH_PER_CHUNK) {
|
|
269
|
-
const size = this.size();
|
|
270
|
-
if (size > maxLength) {
|
|
271
|
-
const fieldsArray = [];
|
|
272
|
-
const scopes = Object.keys(this.listIdToFieldsMap);
|
|
273
|
-
for (let i = 0, len = scopes.length; i < len; i += 1) {
|
|
274
|
-
const { scope, fields } = this.listIdToFieldsMap[scopes[i]];
|
|
275
|
-
Object.keys(fields).forEach((field) => {
|
|
276
|
-
fieldsArray.push([scope, field]);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
// Formula: # of fields per chunk = floor( max length per chunk / avg field length)
|
|
280
|
-
const averageFieldStringLength = size / fieldsArray.length;
|
|
281
|
-
const fieldsPerChunk = Math.floor(maxLength / averageFieldStringLength);
|
|
282
|
-
let j = fieldsPerChunk;
|
|
283
|
-
let result = [];
|
|
284
|
-
let current = null;
|
|
285
|
-
for (let i = 0, len = fieldsArray.length; i < len; i += 1) {
|
|
286
|
-
const scope = fieldsArray[i][0];
|
|
287
|
-
const field = fieldsArray[i][1];
|
|
288
|
-
if (current === null || !current.listIdToFieldsMap[scope]) {
|
|
289
|
-
j = fieldsPerChunk;
|
|
290
|
-
current = new ScopedFieldsCollection();
|
|
291
|
-
current.listIdToFieldsMap[scope] = new ScopedFields(scope, [field]);
|
|
292
|
-
result.push(current);
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
current.listIdToFieldsMap[scope].addField(field);
|
|
296
|
-
}
|
|
297
|
-
j--;
|
|
298
|
-
if (j === 0) {
|
|
299
|
-
current = null;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return result;
|
|
303
|
-
}
|
|
304
|
-
else if (size > 0) {
|
|
305
|
-
return [this];
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
return [];
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
size() {
|
|
312
|
-
return this.toQueryParameterValue().length;
|
|
313
|
-
}
|
|
314
|
-
countOfUnScoped() {
|
|
315
|
-
let count = 0;
|
|
316
|
-
const fieldsArray = Object.values(this.listIdToFieldsMap);
|
|
317
|
-
for (let i = 0, len = fieldsArray.length; i < len; i += 1) {
|
|
318
|
-
if (fieldsArray[i].isUnScoped()) {
|
|
319
|
-
count++;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return count;
|
|
323
|
-
}
|
|
324
|
-
countOfScoped() {
|
|
325
|
-
return Object.keys.length - this.countOfUnScoped();
|
|
326
|
-
}
|
|
327
|
-
isUnScopedMixedWithScoped() {
|
|
328
|
-
return this.countOfUnScoped() > 0 && this.countOfScoped() > 0;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
*
|
|
332
|
-
* @param paramValue like Contacts:Contact.Id,Contact.Name;Opportunities:Opportunity.Id
|
|
333
|
-
* @returns
|
|
334
|
-
*/
|
|
335
|
-
static fromQueryParameterValue(paramValue) {
|
|
336
|
-
const result = new ScopedFieldsCollection();
|
|
337
|
-
if (paramValue) {
|
|
338
|
-
const relativeListChunks = paramValue.split(SEPARATOR_BETWEEN_SCOPES);
|
|
339
|
-
if (relativeListChunks.length === 0)
|
|
340
|
-
return result;
|
|
341
|
-
for (let i = 0, len = relativeListChunks.length; i < len; i += 1) {
|
|
342
|
-
const parsed = ScopedFields.fromQueryParameterValue(relativeListChunks[i]);
|
|
343
|
-
if (parsed) {
|
|
344
|
-
result.listIdToFieldsMap[parsed.scope] = parsed;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
if (result.isUnScopedMixedWithScoped()) {
|
|
348
|
-
throw Error('mixing scoped and unscoped field list is not allowed.');
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
161
|
+
/**
|
|
162
|
+
* related-list-records/batch fields and optional fields could be like below.
|
|
163
|
+
* /ui-api/related-list-records/batch/001R0000006l1xKIAQ/Contacts,Opportunities?
|
|
164
|
+
* optionalFields=Contacts:Contact.Id,Contact.Name;Opportunities:Opportunity.Id
|
|
165
|
+
*
|
|
166
|
+
* the pattern is [{relativeListId}:{[fields]};{relativeListId}:{[fields]}]
|
|
167
|
+
*/
|
|
168
|
+
const SEPARATOR_BETWEEN_SCOPES = ';';
|
|
169
|
+
const SEPARATOR_BETWEEN_SCOPE_AND_FIELDS = ':';
|
|
170
|
+
const SEPARATOR_BETWEEN_FIELDS = ',';
|
|
171
|
+
const UNSCOPED_IDENTIFIER = 'unscoped';
|
|
172
|
+
class ScopedFields {
|
|
173
|
+
constructor(scope, fields) {
|
|
174
|
+
this.fields = {};
|
|
175
|
+
this.scope = scope;
|
|
176
|
+
for (let i = 0, len = fields.length; i < len; i += 1) {
|
|
177
|
+
this.fields[fields[i]] = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
isUnScoped() {
|
|
181
|
+
return this.scope === UNSCOPED_IDENTIFIER;
|
|
182
|
+
}
|
|
183
|
+
addField(field) {
|
|
184
|
+
this.fields[field] = true;
|
|
185
|
+
}
|
|
186
|
+
addFields(fields) {
|
|
187
|
+
fields.forEach(this.addField, this);
|
|
188
|
+
}
|
|
189
|
+
toQueryParameterValue() {
|
|
190
|
+
const joinedFields = join.call(Object.keys(this.fields), SEPARATOR_BETWEEN_FIELDS);
|
|
191
|
+
return this.isUnScoped()
|
|
192
|
+
? joinedFields
|
|
193
|
+
: join.call([this.scope, joinedFields], SEPARATOR_BETWEEN_SCOPE_AND_FIELDS);
|
|
194
|
+
}
|
|
195
|
+
toQueryParams() {
|
|
196
|
+
return this.isUnScoped() ? Object.keys(this.fields) : this.toQueryParameterValue();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* parse Contacts:Contact.Id,Contact.Name into a QueryFields
|
|
200
|
+
*/
|
|
201
|
+
static fromQueryParameterValue(paramValue) {
|
|
202
|
+
if (paramValue === null || paramValue === '')
|
|
203
|
+
return;
|
|
204
|
+
const scopeToFields = paramValue.split(SEPARATOR_BETWEEN_SCOPE_AND_FIELDS);
|
|
205
|
+
if (scopeToFields.length === 1) {
|
|
206
|
+
//unscoped
|
|
207
|
+
return new ScopedFields(UNSCOPED_IDENTIFIER, scopeToFields[0].split(SEPARATOR_BETWEEN_FIELDS));
|
|
208
|
+
}
|
|
209
|
+
else if (scopeToFields.length === 2) {
|
|
210
|
+
const scope = scopeToFields[0];
|
|
211
|
+
const fields = scopeToFields[1];
|
|
212
|
+
if (scope === undefined || fields === null)
|
|
213
|
+
return;
|
|
214
|
+
return new ScopedFields(scope, fields.split(SEPARATOR_BETWEEN_FIELDS));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
class ScopedFieldsCollection {
|
|
222
|
+
constructor() {
|
|
223
|
+
this.listIdToFieldsMap = {};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* merge the from ScopedFieldsCollection into current one
|
|
227
|
+
* @param from
|
|
228
|
+
*/
|
|
229
|
+
merge(from) {
|
|
230
|
+
const { listIdToFieldsMap } = from;
|
|
231
|
+
const scopes = Object.keys(listIdToFieldsMap);
|
|
232
|
+
for (let i = 0, len = scopes.length; i < len; i += 1) {
|
|
233
|
+
const scope = scopes[i];
|
|
234
|
+
const scopedFields = listIdToFieldsMap[scope];
|
|
235
|
+
const existingScopedFields = this.listIdToFieldsMap[scope];
|
|
236
|
+
if (existingScopedFields) {
|
|
237
|
+
existingScopedFields.addFields(Object.keys(scopedFields.fields));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.listIdToFieldsMap[scope] = scopedFields;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
toQueryParams() {
|
|
245
|
+
return this.countOfUnScoped() > 0
|
|
246
|
+
? this.listIdToFieldsMap[UNSCOPED_IDENTIFIER].toQueryParams()
|
|
247
|
+
: this.toQueryParameterValue();
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* convert to query parameter value
|
|
251
|
+
* @returns
|
|
252
|
+
*/
|
|
253
|
+
toQueryParameterValue() {
|
|
254
|
+
let result = [];
|
|
255
|
+
const scopes = Object.keys(this.listIdToFieldsMap);
|
|
256
|
+
for (let i = 0, len = scopes.length; i < len; i += 1) {
|
|
257
|
+
const chunk = this.listIdToFieldsMap[scopes[i]].toQueryParameterValue();
|
|
258
|
+
if (chunk !== undefined)
|
|
259
|
+
result.push(chunk);
|
|
260
|
+
}
|
|
261
|
+
return join.call(result, SEPARATOR_BETWEEN_SCOPES);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* split the ScopedFields into multiple ScopedFields
|
|
265
|
+
* which there max fields list length will not exceeded the specified maxLength
|
|
266
|
+
* @param maxLength
|
|
267
|
+
*/
|
|
268
|
+
split(maxLength = MAX_STRING_LENGTH_PER_CHUNK) {
|
|
269
|
+
const size = this.size();
|
|
270
|
+
if (size > maxLength) {
|
|
271
|
+
const fieldsArray = [];
|
|
272
|
+
const scopes = Object.keys(this.listIdToFieldsMap);
|
|
273
|
+
for (let i = 0, len = scopes.length; i < len; i += 1) {
|
|
274
|
+
const { scope, fields } = this.listIdToFieldsMap[scopes[i]];
|
|
275
|
+
Object.keys(fields).forEach((field) => {
|
|
276
|
+
fieldsArray.push([scope, field]);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// Formula: # of fields per chunk = floor( max length per chunk / avg field length)
|
|
280
|
+
const averageFieldStringLength = size / fieldsArray.length;
|
|
281
|
+
const fieldsPerChunk = Math.floor(maxLength / averageFieldStringLength);
|
|
282
|
+
let j = fieldsPerChunk;
|
|
283
|
+
let result = [];
|
|
284
|
+
let current = null;
|
|
285
|
+
for (let i = 0, len = fieldsArray.length; i < len; i += 1) {
|
|
286
|
+
const scope = fieldsArray[i][0];
|
|
287
|
+
const field = fieldsArray[i][1];
|
|
288
|
+
if (current === null || !current.listIdToFieldsMap[scope]) {
|
|
289
|
+
j = fieldsPerChunk;
|
|
290
|
+
current = new ScopedFieldsCollection();
|
|
291
|
+
current.listIdToFieldsMap[scope] = new ScopedFields(scope, [field]);
|
|
292
|
+
result.push(current);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
current.listIdToFieldsMap[scope].addField(field);
|
|
296
|
+
}
|
|
297
|
+
j--;
|
|
298
|
+
if (j === 0) {
|
|
299
|
+
current = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
else if (size > 0) {
|
|
305
|
+
return [this];
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
size() {
|
|
312
|
+
return this.toQueryParameterValue().length;
|
|
313
|
+
}
|
|
314
|
+
countOfUnScoped() {
|
|
315
|
+
let count = 0;
|
|
316
|
+
const fieldsArray = Object.values(this.listIdToFieldsMap);
|
|
317
|
+
for (let i = 0, len = fieldsArray.length; i < len; i += 1) {
|
|
318
|
+
if (fieldsArray[i].isUnScoped()) {
|
|
319
|
+
count++;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return count;
|
|
323
|
+
}
|
|
324
|
+
countOfScoped() {
|
|
325
|
+
return Object.keys.length - this.countOfUnScoped();
|
|
326
|
+
}
|
|
327
|
+
isUnScopedMixedWithScoped() {
|
|
328
|
+
return this.countOfUnScoped() > 0 && this.countOfScoped() > 0;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
*
|
|
332
|
+
* @param paramValue like Contacts:Contact.Id,Contact.Name;Opportunities:Opportunity.Id
|
|
333
|
+
* @returns
|
|
334
|
+
*/
|
|
335
|
+
static fromQueryParameterValue(paramValue) {
|
|
336
|
+
const result = new ScopedFieldsCollection();
|
|
337
|
+
if (paramValue) {
|
|
338
|
+
const relativeListChunks = paramValue.split(SEPARATOR_BETWEEN_SCOPES);
|
|
339
|
+
if (relativeListChunks.length === 0)
|
|
340
|
+
return result;
|
|
341
|
+
for (let i = 0, len = relativeListChunks.length; i < len; i += 1) {
|
|
342
|
+
const parsed = ScopedFields.fromQueryParameterValue(relativeListChunks[i]);
|
|
343
|
+
if (parsed) {
|
|
344
|
+
result.listIdToFieldsMap[parsed.scope] = parsed;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (result.isUnScopedMixedWithScoped()) {
|
|
348
|
+
throw Error('mixing scoped and unscoped field list is not allowed.');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
const MAX_STRING_LENGTH_PER_CHUNK = 10000;
|
|
356
|
-
const PARSE_ERROR = 'PARSE_AGGREGATE_UI_RESPONSE_ERROR';
|
|
357
|
-
function isErrorResponse(response) {
|
|
358
|
-
return response.httpStatusCode >= 400;
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* merge the aggregate ui child responses into a single object representation
|
|
362
|
-
* @param response
|
|
363
|
-
* @param mergeFunc the function used to define how the records should be merged together
|
|
364
|
-
* @returns the merged record
|
|
365
|
-
*/
|
|
366
|
-
function mergeAggregateUiResponse(response, mergeFunc) {
|
|
367
|
-
const { body } = response;
|
|
368
|
-
try {
|
|
369
|
-
if (body === null ||
|
|
370
|
-
body === undefined ||
|
|
371
|
-
body.compositeResponse === undefined ||
|
|
372
|
-
body.compositeResponse.length === 0) {
|
|
373
|
-
// We shouldn't even get into this state - a 200 with no body?
|
|
374
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
375
|
-
throw new Error('No response body in executeAggregateUi found');
|
|
376
|
-
}
|
|
377
|
-
// if the body has any non-2xx statuses then that's an error and we return
|
|
378
|
-
// the network response with that error
|
|
379
|
-
const error = body.compositeResponse.find(isErrorResponse);
|
|
380
|
-
if (error !== undefined) {
|
|
381
|
-
const { httpStatusCode, body: errorBody } = error;
|
|
382
|
-
const statusText = errorBody.length > 0 ? errorBody[0].errorCode : '';
|
|
383
|
-
return {
|
|
384
|
-
...response,
|
|
385
|
-
ok: false,
|
|
386
|
-
status: httpStatusCode,
|
|
387
|
-
statusText,
|
|
388
|
-
body: errorBody,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
// if we got here there are no errors in body, cast as such
|
|
392
|
-
const responses = body.compositeResponse;
|
|
393
|
-
const merged = responses.reduce((seed, resp) => {
|
|
394
|
-
if (seed === null) {
|
|
395
|
-
return resp.body;
|
|
396
|
-
}
|
|
397
|
-
return mergeFunc(seed, resp.body);
|
|
398
|
-
}, null);
|
|
399
|
-
return {
|
|
400
|
-
...response,
|
|
401
|
-
body: merged,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
return {
|
|
406
|
-
...response,
|
|
407
|
-
ok: false,
|
|
408
|
-
status: HttpStatusCode.ServerError,
|
|
409
|
-
statusText: PARSE_ERROR,
|
|
410
|
-
body: [
|
|
411
|
-
{
|
|
412
|
-
errorCode: PARSE_ERROR,
|
|
413
|
-
message: error.toString(),
|
|
414
|
-
},
|
|
415
|
-
],
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function buildAggregateUiUrl(params, resourceRequest) {
|
|
420
|
-
const { fields, optionalFields } = params;
|
|
421
|
-
const mergedParams = {
|
|
422
|
-
...resourceRequest.queryParams,
|
|
423
|
-
fields,
|
|
424
|
-
optionalFields,
|
|
425
|
-
};
|
|
426
|
-
const queryString = [];
|
|
427
|
-
for (const [key, value] of entries(mergedParams)) {
|
|
428
|
-
if (value !== undefined) {
|
|
429
|
-
queryString.push(`${key}=${isArray(value) ? value.join(',') : value}`);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return `${resourceRequest.baseUri}${resourceRequest.basePath}?${join.call(queryString, '&')}`;
|
|
433
|
-
}
|
|
434
|
-
function shouldUseAggregateUiForFields(fieldsArray, optionalFieldsArray) {
|
|
435
|
-
return fieldsArray.length + optionalFieldsArray.length >= MAX_STRING_LENGTH_PER_CHUNK;
|
|
436
|
-
}
|
|
437
|
-
function isSpanningRecord(fieldValue) {
|
|
438
|
-
return fieldValue !== null && typeof fieldValue === 'object';
|
|
439
|
-
}
|
|
440
|
-
function mergeRecordFields(first, second) {
|
|
441
|
-
const { fields: targetFields } = first;
|
|
442
|
-
const { fields: sourceFields } = second;
|
|
443
|
-
const fieldNames = keys(sourceFields);
|
|
444
|
-
for (let i = 0, len = fieldNames.length; i < len; i += 1) {
|
|
445
|
-
const fieldName = fieldNames[i];
|
|
446
|
-
const sourceField = sourceFields[fieldName];
|
|
447
|
-
const targetField = targetFields[fieldName];
|
|
448
|
-
if (isSpanningRecord(sourceField.value)) {
|
|
449
|
-
if (targetField === undefined) {
|
|
450
|
-
targetFields[fieldName] = sourceField;
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
mergeRecordFields(targetField.value, sourceField.value);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
targetFields[fieldName] = sourceField;
|
|
457
|
-
}
|
|
458
|
-
return first;
|
|
459
|
-
}
|
|
460
|
-
function mergeBatchRecordsFields(first, second, collectionMergeFunc) {
|
|
461
|
-
const { results: targetResults } = first;
|
|
462
|
-
const { results: sourceResults } = second;
|
|
463
|
-
for (let i = 0, len = targetResults.length; i < len; i += 1) {
|
|
464
|
-
const targetResult = targetResults[i];
|
|
465
|
-
const sourceResult = sourceResults[i];
|
|
466
|
-
if (targetResult.statusCode !== HttpStatusCode.Ok)
|
|
467
|
-
continue;
|
|
468
|
-
if (sourceResult.statusCode !== HttpStatusCode.Ok) {
|
|
469
|
-
targetResults[i] = sourceResult;
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
collectionMergeFunc(targetResult.result, sourceResult.result);
|
|
473
|
-
}
|
|
474
|
-
return first;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Check to see if we have fields that are > max allowed characters long
|
|
478
|
-
* @param resourceRequest resource request to check
|
|
479
|
-
* @param endpoint Regular Expression to check the endpoint to aggregate
|
|
480
|
-
* @returns undefined if we should not aggregate. object if we should.
|
|
481
|
-
*/
|
|
482
|
-
function createAggregateBatchRequestInfo(resourceRequest, endpoint) {
|
|
483
|
-
// only handle GETs on the given endpoint regex
|
|
484
|
-
if (!isGetRequestForEndpoint(endpoint, resourceRequest)) {
|
|
485
|
-
return undefined;
|
|
486
|
-
}
|
|
487
|
-
const { queryParams: { fields, optionalFields }, } = resourceRequest;
|
|
488
|
-
// only handle requests with fields or optional fields
|
|
489
|
-
if (fields === undefined && optionalFields === undefined) {
|
|
490
|
-
return undefined;
|
|
491
|
-
}
|
|
492
|
-
const fieldsArray = arrayOrEmpty(fields);
|
|
493
|
-
const optionalFieldsArray = arrayOrEmpty(optionalFields);
|
|
494
|
-
// if fields and optional fields are empty delegate request
|
|
495
|
-
if (fieldsArray.length === 0 && optionalFieldsArray.length === 0) {
|
|
496
|
-
return undefined;
|
|
497
|
-
}
|
|
498
|
-
const fieldsString = fieldsArray.join(',');
|
|
499
|
-
const optionalFieldsString = optionalFieldsArray.join(',');
|
|
500
|
-
const shouldUseAggregate = shouldUseAggregateUiForFields(fieldsString, optionalFieldsString);
|
|
501
|
-
if (!shouldUseAggregate) {
|
|
502
|
-
return undefined;
|
|
503
|
-
}
|
|
504
|
-
const fieldCollection = ScopedFieldsCollection.fromQueryParameterValue(fieldsString).split(MAX_STRING_LENGTH_PER_CHUNK);
|
|
505
|
-
const optionalFieldCollection = ScopedFieldsCollection.fromQueryParameterValue(optionalFieldsString).split(MAX_STRING_LENGTH_PER_CHUNK);
|
|
506
|
-
return {
|
|
507
|
-
fieldCollection,
|
|
508
|
-
optionalFieldCollection,
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function createAggregateUiRequest(resourceRequest, compositeRequest) {
|
|
512
|
-
const aggregateUiPostBody = { compositeRequest };
|
|
513
|
-
const aggregateResourceRequest = {
|
|
514
|
-
method: 'post',
|
|
515
|
-
baseUri: resourceRequest.baseUri,
|
|
516
|
-
basePath: '/ui-api/aggregate-ui',
|
|
517
|
-
body: aggregateUiPostBody,
|
|
518
|
-
priority: resourceRequest.priority,
|
|
519
|
-
queryParams: {},
|
|
520
|
-
headers: {},
|
|
521
|
-
urlParams: {},
|
|
522
|
-
};
|
|
523
|
-
return aggregateResourceRequest;
|
|
524
|
-
}
|
|
525
|
-
function buildCompositeRequestByFields(referenceId, resourceRequest, recordsCompositeRequest) {
|
|
526
|
-
const { fieldCollection, optionalFieldCollection } = recordsCompositeRequest;
|
|
527
|
-
const compositeRequest = [];
|
|
528
|
-
if (fieldCollection !== undefined) {
|
|
529
|
-
for (let i = 0, len = fieldCollection.length; i < len; i += 1) {
|
|
530
|
-
const fieldChunk = fieldCollection[i].toQueryParams();
|
|
531
|
-
if (fieldChunk.length === 0) {
|
|
532
|
-
continue;
|
|
533
|
-
}
|
|
534
|
-
const url = buildAggregateUiUrl({
|
|
535
|
-
fields: fieldChunk,
|
|
536
|
-
}, resourceRequest);
|
|
537
|
-
push.call(compositeRequest, {
|
|
538
|
-
url,
|
|
539
|
-
referenceId: `${referenceId}_fields_${i}`,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
if (optionalFieldCollection !== undefined) {
|
|
544
|
-
for (let i = 0, len = optionalFieldCollection.length; i < len; i += 1) {
|
|
545
|
-
const fieldChunk = optionalFieldCollection[i].toQueryParams();
|
|
546
|
-
if (fieldChunk.length === 0) {
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
const url = buildAggregateUiUrl({
|
|
550
|
-
optionalFields: fieldChunk,
|
|
551
|
-
}, resourceRequest);
|
|
552
|
-
push.call(compositeRequest, {
|
|
553
|
-
url,
|
|
554
|
-
referenceId: `${referenceId}_optionalFields_${i}`,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return compositeRequest;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Checks if a resource request is a GET method on the given endpoint
|
|
562
|
-
* @param endpoint Regular Expression of the endpoint
|
|
563
|
-
* @param request the resource request
|
|
564
|
-
*/
|
|
565
|
-
function isGetRequestForEndpoint(endpoint, request) {
|
|
566
|
-
const { basePath, method } = request;
|
|
567
|
-
return endpoint.test(basePath) && method === 'get';
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Checks if any is an array and returns it as an array.
|
|
571
|
-
* if not an array it returns an empty array.
|
|
572
|
-
* @param array the item to check is an array
|
|
573
|
-
* @returns the array or an empty array
|
|
574
|
-
*/
|
|
575
|
-
function arrayOrEmpty(array) {
|
|
576
|
-
return array !== undefined && isArray(array) ? array : [];
|
|
355
|
+
const MAX_STRING_LENGTH_PER_CHUNK = 10000;
|
|
356
|
+
const PARSE_ERROR = 'PARSE_AGGREGATE_UI_RESPONSE_ERROR';
|
|
357
|
+
function isErrorResponse(response) {
|
|
358
|
+
return response.httpStatusCode >= 400;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* merge the aggregate ui child responses into a single object representation
|
|
362
|
+
* @param response
|
|
363
|
+
* @param mergeFunc the function used to define how the records should be merged together
|
|
364
|
+
* @returns the merged record
|
|
365
|
+
*/
|
|
366
|
+
function mergeAggregateUiResponse(response, mergeFunc) {
|
|
367
|
+
const { body } = response;
|
|
368
|
+
try {
|
|
369
|
+
if (body === null ||
|
|
370
|
+
body === undefined ||
|
|
371
|
+
body.compositeResponse === undefined ||
|
|
372
|
+
body.compositeResponse.length === 0) {
|
|
373
|
+
// We shouldn't even get into this state - a 200 with no body?
|
|
374
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
375
|
+
throw new Error('No response body in executeAggregateUi found');
|
|
376
|
+
}
|
|
377
|
+
// if the body has any non-2xx statuses then that's an error and we return
|
|
378
|
+
// the network response with that error
|
|
379
|
+
const error = body.compositeResponse.find(isErrorResponse);
|
|
380
|
+
if (error !== undefined) {
|
|
381
|
+
const { httpStatusCode, body: errorBody } = error;
|
|
382
|
+
const statusText = errorBody.length > 0 ? errorBody[0].errorCode : '';
|
|
383
|
+
return {
|
|
384
|
+
...response,
|
|
385
|
+
ok: false,
|
|
386
|
+
status: httpStatusCode,
|
|
387
|
+
statusText,
|
|
388
|
+
body: errorBody,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// if we got here there are no errors in body, cast as such
|
|
392
|
+
const responses = body.compositeResponse;
|
|
393
|
+
const merged = responses.reduce((seed, resp) => {
|
|
394
|
+
if (seed === null) {
|
|
395
|
+
return resp.body;
|
|
396
|
+
}
|
|
397
|
+
return mergeFunc(seed, resp.body);
|
|
398
|
+
}, null);
|
|
399
|
+
return {
|
|
400
|
+
...response,
|
|
401
|
+
body: merged,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
return {
|
|
406
|
+
...response,
|
|
407
|
+
ok: false,
|
|
408
|
+
status: HttpStatusCode.ServerError,
|
|
409
|
+
statusText: PARSE_ERROR,
|
|
410
|
+
body: [
|
|
411
|
+
{
|
|
412
|
+
errorCode: PARSE_ERROR,
|
|
413
|
+
message: error.toString(),
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function buildAggregateUiUrl(params, resourceRequest) {
|
|
420
|
+
const { fields, optionalFields } = params;
|
|
421
|
+
const mergedParams = {
|
|
422
|
+
...resourceRequest.queryParams,
|
|
423
|
+
fields,
|
|
424
|
+
optionalFields,
|
|
425
|
+
};
|
|
426
|
+
const queryString = [];
|
|
427
|
+
for (const [key, value] of entries(mergedParams)) {
|
|
428
|
+
if (value !== undefined) {
|
|
429
|
+
queryString.push(`${key}=${isArray(value) ? value.join(',') : value}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return `${resourceRequest.baseUri}${resourceRequest.basePath}?${join.call(queryString, '&')}`;
|
|
433
|
+
}
|
|
434
|
+
function shouldUseAggregateUiForFields(fieldsArray, optionalFieldsArray) {
|
|
435
|
+
return fieldsArray.length + optionalFieldsArray.length >= MAX_STRING_LENGTH_PER_CHUNK;
|
|
436
|
+
}
|
|
437
|
+
function isSpanningRecord(fieldValue) {
|
|
438
|
+
return fieldValue !== null && typeof fieldValue === 'object';
|
|
439
|
+
}
|
|
440
|
+
function mergeRecordFields(first, second) {
|
|
441
|
+
const { fields: targetFields } = first;
|
|
442
|
+
const { fields: sourceFields } = second;
|
|
443
|
+
const fieldNames = keys(sourceFields);
|
|
444
|
+
for (let i = 0, len = fieldNames.length; i < len; i += 1) {
|
|
445
|
+
const fieldName = fieldNames[i];
|
|
446
|
+
const sourceField = sourceFields[fieldName];
|
|
447
|
+
const targetField = targetFields[fieldName];
|
|
448
|
+
if (isSpanningRecord(sourceField.value)) {
|
|
449
|
+
if (targetField === undefined) {
|
|
450
|
+
targetFields[fieldName] = sourceField;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
mergeRecordFields(targetField.value, sourceField.value);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
targetFields[fieldName] = sourceField;
|
|
457
|
+
}
|
|
458
|
+
return first;
|
|
459
|
+
}
|
|
460
|
+
function mergeBatchRecordsFields(first, second, collectionMergeFunc) {
|
|
461
|
+
const { results: targetResults } = first;
|
|
462
|
+
const { results: sourceResults } = second;
|
|
463
|
+
for (let i = 0, len = targetResults.length; i < len; i += 1) {
|
|
464
|
+
const targetResult = targetResults[i];
|
|
465
|
+
const sourceResult = sourceResults[i];
|
|
466
|
+
if (targetResult.statusCode !== HttpStatusCode.Ok)
|
|
467
|
+
continue;
|
|
468
|
+
if (sourceResult.statusCode !== HttpStatusCode.Ok) {
|
|
469
|
+
targetResults[i] = sourceResult;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
collectionMergeFunc(targetResult.result, sourceResult.result);
|
|
473
|
+
}
|
|
474
|
+
return first;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Check to see if we have fields that are > max allowed characters long
|
|
478
|
+
* @param resourceRequest resource request to check
|
|
479
|
+
* @param endpoint Regular Expression to check the endpoint to aggregate
|
|
480
|
+
* @returns undefined if we should not aggregate. object if we should.
|
|
481
|
+
*/
|
|
482
|
+
function createAggregateBatchRequestInfo(resourceRequest, endpoint) {
|
|
483
|
+
// only handle GETs on the given endpoint regex
|
|
484
|
+
if (!isGetRequestForEndpoint(endpoint, resourceRequest)) {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
const { queryParams: { fields, optionalFields }, } = resourceRequest;
|
|
488
|
+
// only handle requests with fields or optional fields
|
|
489
|
+
if (fields === undefined && optionalFields === undefined) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
const fieldsArray = arrayOrEmpty(fields);
|
|
493
|
+
const optionalFieldsArray = arrayOrEmpty(optionalFields);
|
|
494
|
+
// if fields and optional fields are empty delegate request
|
|
495
|
+
if (fieldsArray.length === 0 && optionalFieldsArray.length === 0) {
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
const fieldsString = fieldsArray.join(',');
|
|
499
|
+
const optionalFieldsString = optionalFieldsArray.join(',');
|
|
500
|
+
const shouldUseAggregate = shouldUseAggregateUiForFields(fieldsString, optionalFieldsString);
|
|
501
|
+
if (!shouldUseAggregate) {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
const fieldCollection = ScopedFieldsCollection.fromQueryParameterValue(fieldsString).split(MAX_STRING_LENGTH_PER_CHUNK);
|
|
505
|
+
const optionalFieldCollection = ScopedFieldsCollection.fromQueryParameterValue(optionalFieldsString).split(MAX_STRING_LENGTH_PER_CHUNK);
|
|
506
|
+
return {
|
|
507
|
+
fieldCollection,
|
|
508
|
+
optionalFieldCollection,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function createAggregateUiRequest(resourceRequest, compositeRequest) {
|
|
512
|
+
const aggregateUiPostBody = { compositeRequest };
|
|
513
|
+
const aggregateResourceRequest = {
|
|
514
|
+
method: 'post',
|
|
515
|
+
baseUri: resourceRequest.baseUri,
|
|
516
|
+
basePath: '/ui-api/aggregate-ui',
|
|
517
|
+
body: aggregateUiPostBody,
|
|
518
|
+
priority: resourceRequest.priority,
|
|
519
|
+
queryParams: {},
|
|
520
|
+
headers: {},
|
|
521
|
+
urlParams: {},
|
|
522
|
+
};
|
|
523
|
+
return aggregateResourceRequest;
|
|
524
|
+
}
|
|
525
|
+
function buildCompositeRequestByFields(referenceId, resourceRequest, recordsCompositeRequest) {
|
|
526
|
+
const { fieldCollection, optionalFieldCollection } = recordsCompositeRequest;
|
|
527
|
+
const compositeRequest = [];
|
|
528
|
+
if (fieldCollection !== undefined) {
|
|
529
|
+
for (let i = 0, len = fieldCollection.length; i < len; i += 1) {
|
|
530
|
+
const fieldChunk = fieldCollection[i].toQueryParams();
|
|
531
|
+
if (fieldChunk.length === 0) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const url = buildAggregateUiUrl({
|
|
535
|
+
fields: fieldChunk,
|
|
536
|
+
}, resourceRequest);
|
|
537
|
+
push.call(compositeRequest, {
|
|
538
|
+
url,
|
|
539
|
+
referenceId: `${referenceId}_fields_${i}`,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (optionalFieldCollection !== undefined) {
|
|
544
|
+
for (let i = 0, len = optionalFieldCollection.length; i < len; i += 1) {
|
|
545
|
+
const fieldChunk = optionalFieldCollection[i].toQueryParams();
|
|
546
|
+
if (fieldChunk.length === 0) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const url = buildAggregateUiUrl({
|
|
550
|
+
optionalFields: fieldChunk,
|
|
551
|
+
}, resourceRequest);
|
|
552
|
+
push.call(compositeRequest, {
|
|
553
|
+
url,
|
|
554
|
+
referenceId: `${referenceId}_optionalFields_${i}`,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return compositeRequest;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Checks if a resource request is a GET method on the given endpoint
|
|
562
|
+
* @param endpoint Regular Expression of the endpoint
|
|
563
|
+
* @param request the resource request
|
|
564
|
+
*/
|
|
565
|
+
function isGetRequestForEndpoint(endpoint, request) {
|
|
566
|
+
const { basePath, method } = request;
|
|
567
|
+
return endpoint.test(basePath) && method === 'get';
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Checks if any is an array and returns it as an array.
|
|
571
|
+
* if not an array it returns an empty array.
|
|
572
|
+
* @param array the item to check is an array
|
|
573
|
+
* @returns the array or an empty array
|
|
574
|
+
*/
|
|
575
|
+
function arrayOrEmpty(array) {
|
|
576
|
+
return array !== undefined && isArray(array) ? array : [];
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
-
const RECORD_ENDPOINT_REGEX = /^\/ui-api\/records\/?(([a-zA-Z0-9]+))?$/;
|
|
580
|
-
const referenceId$3 = 'LDS_Records_AggregateUi';
|
|
581
|
-
/**
|
|
582
|
-
* Export to facilitate unit tests
|
|
583
|
-
* Merge the second getRecord result into the first one.
|
|
584
|
-
* If any is error response, merged result is error response.
|
|
585
|
-
* If both are sucesses, due to they are from same records,
|
|
586
|
-
* fields sub node will be merged recursively
|
|
587
|
-
*/
|
|
588
|
-
function mergeGetRecordResult(first, second) {
|
|
589
|
-
// return the error if first is error.
|
|
590
|
-
if (isArray(first) && !isArray(second))
|
|
591
|
-
return first;
|
|
592
|
-
// return the error if second is error.
|
|
593
|
-
if (!isArray(first) && isArray(second))
|
|
594
|
-
return second;
|
|
595
|
-
// concat the error array if both are error
|
|
596
|
-
if (isArray(first) && isArray(second)) {
|
|
597
|
-
return [...first, ...second];
|
|
598
|
-
}
|
|
599
|
-
mergeRecordFields(first, second);
|
|
600
|
-
return first;
|
|
601
|
-
}
|
|
602
|
-
function makeNetworkChunkFieldsGetRecord(networkAdapter) {
|
|
603
|
-
return (resourceRequest, resourceRequestContext) => {
|
|
604
|
-
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RECORD_ENDPOINT_REGEX);
|
|
605
|
-
if (batchRequestInfo === undefined) {
|
|
606
|
-
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
607
|
-
}
|
|
608
|
-
const compositeRequest = buildCompositeRequestByFields(referenceId$3, resourceRequest, batchRequestInfo);
|
|
609
|
-
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
610
|
-
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
611
|
-
return mergeAggregateUiResponse(response, mergeGetRecordResult);
|
|
612
|
-
});
|
|
613
|
-
};
|
|
579
|
+
const RECORD_ENDPOINT_REGEX = /^\/ui-api\/records\/?(([a-zA-Z0-9]+))?$/;
|
|
580
|
+
const referenceId$3 = 'LDS_Records_AggregateUi';
|
|
581
|
+
/**
|
|
582
|
+
* Export to facilitate unit tests
|
|
583
|
+
* Merge the second getRecord result into the first one.
|
|
584
|
+
* If any is error response, merged result is error response.
|
|
585
|
+
* If both are sucesses, due to they are from same records,
|
|
586
|
+
* fields sub node will be merged recursively
|
|
587
|
+
*/
|
|
588
|
+
function mergeGetRecordResult(first, second) {
|
|
589
|
+
// return the error if first is error.
|
|
590
|
+
if (isArray(first) && !isArray(second))
|
|
591
|
+
return first;
|
|
592
|
+
// return the error if second is error.
|
|
593
|
+
if (!isArray(first) && isArray(second))
|
|
594
|
+
return second;
|
|
595
|
+
// concat the error array if both are error
|
|
596
|
+
if (isArray(first) && isArray(second)) {
|
|
597
|
+
return [...first, ...second];
|
|
598
|
+
}
|
|
599
|
+
mergeRecordFields(first, second);
|
|
600
|
+
return first;
|
|
601
|
+
}
|
|
602
|
+
function makeNetworkChunkFieldsGetRecord(networkAdapter) {
|
|
603
|
+
return (resourceRequest, resourceRequestContext) => {
|
|
604
|
+
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RECORD_ENDPOINT_REGEX);
|
|
605
|
+
if (batchRequestInfo === undefined) {
|
|
606
|
+
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
607
|
+
}
|
|
608
|
+
const compositeRequest = buildCompositeRequestByFields(referenceId$3, resourceRequest, batchRequestInfo);
|
|
609
|
+
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
610
|
+
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
611
|
+
return mergeAggregateUiResponse(response, mergeGetRecordResult);
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
614
|
}
|
|
615
615
|
|
|
616
|
-
const RECORDS_BATCH_ENDPOINT_REGEX = /^\/ui-api\/records\/batch\/?(([a-zA-Z0-9|,]+))?$/;
|
|
617
|
-
const referenceId$2 = 'LDS_Records_Batch_AggregateUi';
|
|
618
|
-
function makeNetworkChunkFieldsGetRecordsBatch(networkAdapter) {
|
|
619
|
-
return (resourceRequest, resourceRequestContext) => {
|
|
620
|
-
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RECORDS_BATCH_ENDPOINT_REGEX);
|
|
621
|
-
if (batchRequestInfo === undefined) {
|
|
622
|
-
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
623
|
-
}
|
|
624
|
-
const compositeRequest = buildCompositeRequestByFields(referenceId$2, resourceRequest, batchRequestInfo);
|
|
625
|
-
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
626
|
-
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
627
|
-
return mergeAggregateUiResponse(response, (first, second) => {
|
|
628
|
-
return mergeBatchRecordsFields(first, second, (a, b) => {
|
|
629
|
-
return mergeRecordFields(a, b);
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
};
|
|
616
|
+
const RECORDS_BATCH_ENDPOINT_REGEX = /^\/ui-api\/records\/batch\/?(([a-zA-Z0-9|,]+))?$/;
|
|
617
|
+
const referenceId$2 = 'LDS_Records_Batch_AggregateUi';
|
|
618
|
+
function makeNetworkChunkFieldsGetRecordsBatch(networkAdapter) {
|
|
619
|
+
return (resourceRequest, resourceRequestContext) => {
|
|
620
|
+
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RECORDS_BATCH_ENDPOINT_REGEX);
|
|
621
|
+
if (batchRequestInfo === undefined) {
|
|
622
|
+
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
623
|
+
}
|
|
624
|
+
const compositeRequest = buildCompositeRequestByFields(referenceId$2, resourceRequest, batchRequestInfo);
|
|
625
|
+
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
626
|
+
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
627
|
+
return mergeAggregateUiResponse(response, (first, second) => {
|
|
628
|
+
return mergeBatchRecordsFields(first, second, (a, b) => {
|
|
629
|
+
return mergeRecordFields(a, b);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
const RELATED_LIST_RECORDS_ENDPOINT_REGEX = /^\/ui-api\/related-list-records\/?(([a-zA-Z0-9]+))?\/?(([a-zA-Z0-9]+))?$/;
|
|
637
|
-
const referenceId$1 = 'LDS_Related_List_Records_AggregateUi';
|
|
638
|
-
const QUERY_KEY_FIELDS = 'fields';
|
|
639
|
-
const QUERY_KEY_OPTIONAL_FIELDS = 'optionalFields';
|
|
640
|
-
/**
|
|
641
|
-
* Merge the second related list record collection into first one and return it.
|
|
642
|
-
* It checks both collections should have exaction same records, otherwise error.
|
|
643
|
-
* Exports it for unit tests
|
|
644
|
-
*/
|
|
645
|
-
function mergeRelatedRecordsFields(first, second) {
|
|
646
|
-
const { records: targetRecords } = first;
|
|
647
|
-
const { records: sourceRecords } = second;
|
|
648
|
-
if (sourceRecords.length === targetRecords.length &&
|
|
649
|
-
recordIdsAllMatch(targetRecords, sourceRecords)) {
|
|
650
|
-
first.fields = first.fields.concat(second.fields);
|
|
651
|
-
first.optionalFields = first.optionalFields.concat(second.optionalFields);
|
|
652
|
-
for (let i = 0, len = sourceRecords.length; i < len; i += 1) {
|
|
653
|
-
const targetRecord = targetRecords[i];
|
|
654
|
-
const sourceRecord = sourceRecords[i];
|
|
655
|
-
mergeRecordFields(targetRecord, sourceRecord);
|
|
656
|
-
}
|
|
657
|
-
mergePageUrls(first, second);
|
|
658
|
-
return first;
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
// Throw error due to two collection are about different set of records
|
|
662
|
-
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
663
|
-
throw new Error('Aggregate UI response is invalid');
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
function makeNetworkChunkFieldsGetRelatedListRecords(networkAdapter) {
|
|
667
|
-
return (resourceRequest, resourceRequestContext) => {
|
|
668
|
-
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RELATED_LIST_RECORDS_ENDPOINT_REGEX);
|
|
669
|
-
if (batchRequestInfo === undefined) {
|
|
670
|
-
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
671
|
-
}
|
|
672
|
-
const compositeRequest = buildCompositeRequestByFields(referenceId$1, resourceRequest, batchRequestInfo);
|
|
673
|
-
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
674
|
-
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
675
|
-
return mergeAggregateUiResponse(response, mergeRelatedRecordsFields);
|
|
676
|
-
});
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* merge the second related list record collection into first one and return it
|
|
681
|
-
*/
|
|
682
|
-
function mergePageUrls(first, second) {
|
|
683
|
-
first.currentPageUrl = mergeUrl(first.currentPageUrl, second.currentPageUrl);
|
|
684
|
-
first.previousPageUrl = mergeUrl(first.previousPageUrl, second.previousPageUrl);
|
|
685
|
-
first.nextPageUrl = mergeUrl(first.nextPageUrl, second.nextPageUrl);
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* merge to paging url with different set of fields or optional fields as combined one
|
|
689
|
-
* the paging url is like
|
|
690
|
-
* /services/data/v58.0/ui-api/related-list-records/001R0000006l1xKIAQ/Contacts
|
|
691
|
-
* ?fields=Id%2CName&optionalFields=Contact.Id%2CContact.Name&pageSize=50&pageToken=0
|
|
692
|
-
* @param path1 url path and query parmeter without domain
|
|
693
|
-
* @param path2 url path and query parmeter without domain
|
|
694
|
-
*
|
|
695
|
-
* Export to unit test
|
|
696
|
-
*/
|
|
697
|
-
function mergeUrl(path1, path2) {
|
|
698
|
-
if (path1 === null)
|
|
699
|
-
return path2;
|
|
700
|
-
if (path2 === null)
|
|
701
|
-
return path1;
|
|
702
|
-
// new Url(...) need the path1, path2 to be prefix-ed with this fake domain
|
|
703
|
-
const domain = 'http://c.com';
|
|
704
|
-
const url1 = new URL(domain + path1);
|
|
705
|
-
const url2 = new URL(domain + path2);
|
|
706
|
-
const searchParams1 = url1.searchParams;
|
|
707
|
-
const fields = mergeFields(url1, url2, QUERY_KEY_FIELDS);
|
|
708
|
-
if (fields && searchParams1.get(QUERY_KEY_FIELDS) !== fields) {
|
|
709
|
-
searchParams1.set(QUERY_KEY_FIELDS, fields);
|
|
710
|
-
}
|
|
711
|
-
const optionalFields = mergeFields(url1, url2, QUERY_KEY_OPTIONAL_FIELDS);
|
|
712
|
-
if (optionalFields && searchParams1.get(QUERY_KEY_OPTIONAL_FIELDS) !== optionalFields) {
|
|
713
|
-
searchParams1.set(QUERY_KEY_OPTIONAL_FIELDS, optionalFields);
|
|
714
|
-
}
|
|
715
|
-
from(searchParams1.keys())
|
|
716
|
-
.sort()
|
|
717
|
-
.forEach((key) => {
|
|
718
|
-
const value = searchParams1.get(key);
|
|
719
|
-
searchParams1.delete(key);
|
|
720
|
-
searchParams1.append(key, value);
|
|
721
|
-
});
|
|
722
|
-
return url1.toString().substr(domain.length);
|
|
723
|
-
}
|
|
724
|
-
function mergeFields(url1, url2, name) {
|
|
725
|
-
const fields1 = ScopedFieldsCollection.fromQueryParameterValue(url1.searchParams.get(name));
|
|
726
|
-
const fields2 = ScopedFieldsCollection.fromQueryParameterValue(url2.searchParams.get(name));
|
|
727
|
-
fields1.merge(fields2);
|
|
728
|
-
return fields1.toQueryParameterValue();
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Checks that all records ids exist in both arrays
|
|
732
|
-
* @param first batch of first array or records
|
|
733
|
-
* @param second batch of second array or records
|
|
734
|
-
* @returns
|
|
735
|
-
*/
|
|
736
|
-
function recordIdsAllMatch(first, second) {
|
|
737
|
-
const firstIds = first.map((record) => record.id);
|
|
738
|
-
const secondIds = second.map((record) => record.id);
|
|
739
|
-
return firstIds.every((id) => secondIds.includes(id));
|
|
636
|
+
const RELATED_LIST_RECORDS_ENDPOINT_REGEX = /^\/ui-api\/related-list-records\/?(([a-zA-Z0-9]+))?\/?(([a-zA-Z0-9]+))?$/;
|
|
637
|
+
const referenceId$1 = 'LDS_Related_List_Records_AggregateUi';
|
|
638
|
+
const QUERY_KEY_FIELDS = 'fields';
|
|
639
|
+
const QUERY_KEY_OPTIONAL_FIELDS = 'optionalFields';
|
|
640
|
+
/**
|
|
641
|
+
* Merge the second related list record collection into first one and return it.
|
|
642
|
+
* It checks both collections should have exaction same records, otherwise error.
|
|
643
|
+
* Exports it for unit tests
|
|
644
|
+
*/
|
|
645
|
+
function mergeRelatedRecordsFields(first, second) {
|
|
646
|
+
const { records: targetRecords } = first;
|
|
647
|
+
const { records: sourceRecords } = second;
|
|
648
|
+
if (sourceRecords.length === targetRecords.length &&
|
|
649
|
+
recordIdsAllMatch(targetRecords, sourceRecords)) {
|
|
650
|
+
first.fields = first.fields.concat(second.fields);
|
|
651
|
+
first.optionalFields = first.optionalFields.concat(second.optionalFields);
|
|
652
|
+
for (let i = 0, len = sourceRecords.length; i < len; i += 1) {
|
|
653
|
+
const targetRecord = targetRecords[i];
|
|
654
|
+
const sourceRecord = sourceRecords[i];
|
|
655
|
+
mergeRecordFields(targetRecord, sourceRecord);
|
|
656
|
+
}
|
|
657
|
+
mergePageUrls(first, second);
|
|
658
|
+
return first;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// Throw error due to two collection are about different set of records
|
|
662
|
+
// eslint-disable-next-line @salesforce/lds/no-error-in-production
|
|
663
|
+
throw new Error('Aggregate UI response is invalid');
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function makeNetworkChunkFieldsGetRelatedListRecords(networkAdapter) {
|
|
667
|
+
return (resourceRequest, resourceRequestContext) => {
|
|
668
|
+
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RELATED_LIST_RECORDS_ENDPOINT_REGEX);
|
|
669
|
+
if (batchRequestInfo === undefined) {
|
|
670
|
+
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
671
|
+
}
|
|
672
|
+
const compositeRequest = buildCompositeRequestByFields(referenceId$1, resourceRequest, batchRequestInfo);
|
|
673
|
+
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
674
|
+
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
675
|
+
return mergeAggregateUiResponse(response, mergeRelatedRecordsFields);
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* merge the second related list record collection into first one and return it
|
|
681
|
+
*/
|
|
682
|
+
function mergePageUrls(first, second) {
|
|
683
|
+
first.currentPageUrl = mergeUrl(first.currentPageUrl, second.currentPageUrl);
|
|
684
|
+
first.previousPageUrl = mergeUrl(first.previousPageUrl, second.previousPageUrl);
|
|
685
|
+
first.nextPageUrl = mergeUrl(first.nextPageUrl, second.nextPageUrl);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* merge to paging url with different set of fields or optional fields as combined one
|
|
689
|
+
* the paging url is like
|
|
690
|
+
* /services/data/v58.0/ui-api/related-list-records/001R0000006l1xKIAQ/Contacts
|
|
691
|
+
* ?fields=Id%2CName&optionalFields=Contact.Id%2CContact.Name&pageSize=50&pageToken=0
|
|
692
|
+
* @param path1 url path and query parmeter without domain
|
|
693
|
+
* @param path2 url path and query parmeter without domain
|
|
694
|
+
*
|
|
695
|
+
* Export to unit test
|
|
696
|
+
*/
|
|
697
|
+
function mergeUrl(path1, path2) {
|
|
698
|
+
if (path1 === null)
|
|
699
|
+
return path2;
|
|
700
|
+
if (path2 === null)
|
|
701
|
+
return path1;
|
|
702
|
+
// new Url(...) need the path1, path2 to be prefix-ed with this fake domain
|
|
703
|
+
const domain = 'http://c.com';
|
|
704
|
+
const url1 = new URL(domain + path1);
|
|
705
|
+
const url2 = new URL(domain + path2);
|
|
706
|
+
const searchParams1 = url1.searchParams;
|
|
707
|
+
const fields = mergeFields(url1, url2, QUERY_KEY_FIELDS);
|
|
708
|
+
if (fields && searchParams1.get(QUERY_KEY_FIELDS) !== fields) {
|
|
709
|
+
searchParams1.set(QUERY_KEY_FIELDS, fields);
|
|
710
|
+
}
|
|
711
|
+
const optionalFields = mergeFields(url1, url2, QUERY_KEY_OPTIONAL_FIELDS);
|
|
712
|
+
if (optionalFields && searchParams1.get(QUERY_KEY_OPTIONAL_FIELDS) !== optionalFields) {
|
|
713
|
+
searchParams1.set(QUERY_KEY_OPTIONAL_FIELDS, optionalFields);
|
|
714
|
+
}
|
|
715
|
+
from(searchParams1.keys())
|
|
716
|
+
.sort()
|
|
717
|
+
.forEach((key) => {
|
|
718
|
+
const value = searchParams1.get(key);
|
|
719
|
+
searchParams1.delete(key);
|
|
720
|
+
searchParams1.append(key, value);
|
|
721
|
+
});
|
|
722
|
+
return url1.toString().substr(domain.length);
|
|
723
|
+
}
|
|
724
|
+
function mergeFields(url1, url2, name) {
|
|
725
|
+
const fields1 = ScopedFieldsCollection.fromQueryParameterValue(url1.searchParams.get(name));
|
|
726
|
+
const fields2 = ScopedFieldsCollection.fromQueryParameterValue(url2.searchParams.get(name));
|
|
727
|
+
fields1.merge(fields2);
|
|
728
|
+
return fields1.toQueryParameterValue();
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Checks that all records ids exist in both arrays
|
|
732
|
+
* @param first batch of first array or records
|
|
733
|
+
* @param second batch of second array or records
|
|
734
|
+
* @returns
|
|
735
|
+
*/
|
|
736
|
+
function recordIdsAllMatch(first, second) {
|
|
737
|
+
const firstIds = first.map((record) => record.id);
|
|
738
|
+
const secondIds = second.map((record) => record.id);
|
|
739
|
+
return firstIds.every((id) => secondIds.includes(id));
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
-
const RELATED_LIST_RECORDS_BATCH_ENDPOINT_REGEX = /^\/ui-api\/related-list-records\/batch\/?(([a-zA-Z0-9]+))?\//;
|
|
743
|
-
const referenceId = 'LDS_Related_List_Records_AggregateUi';
|
|
744
|
-
function makeNetworkChunkFieldsGetRelatedListRecordsBatch(networkAdapter) {
|
|
745
|
-
return (resourceRequest, resourceRequestContext) => {
|
|
746
|
-
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RELATED_LIST_RECORDS_BATCH_ENDPOINT_REGEX);
|
|
747
|
-
if (batchRequestInfo === undefined) {
|
|
748
|
-
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
749
|
-
}
|
|
750
|
-
const compositeRequest = buildCompositeRequestByFields(referenceId, resourceRequest, batchRequestInfo);
|
|
751
|
-
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
752
|
-
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
753
|
-
return mergeAggregateUiResponse(response, (first, second) => {
|
|
754
|
-
return mergeBatchRecordsFields(first, second, (a, b) => {
|
|
755
|
-
return mergeRelatedRecordsFields(a, b);
|
|
756
|
-
});
|
|
757
|
-
});
|
|
758
|
-
});
|
|
759
|
-
};
|
|
742
|
+
const RELATED_LIST_RECORDS_BATCH_ENDPOINT_REGEX = /^\/ui-api\/related-list-records\/batch\/?(([a-zA-Z0-9]+))?\//;
|
|
743
|
+
const referenceId = 'LDS_Related_List_Records_AggregateUi';
|
|
744
|
+
function makeNetworkChunkFieldsGetRelatedListRecordsBatch(networkAdapter) {
|
|
745
|
+
return (resourceRequest, resourceRequestContext) => {
|
|
746
|
+
const batchRequestInfo = createAggregateBatchRequestInfo(resourceRequest, RELATED_LIST_RECORDS_BATCH_ENDPOINT_REGEX);
|
|
747
|
+
if (batchRequestInfo === undefined) {
|
|
748
|
+
return networkAdapter(resourceRequest, resourceRequestContext);
|
|
749
|
+
}
|
|
750
|
+
const compositeRequest = buildCompositeRequestByFields(referenceId, resourceRequest, batchRequestInfo);
|
|
751
|
+
const aggregateRequest = createAggregateUiRequest(resourceRequest, compositeRequest);
|
|
752
|
+
return networkAdapter(aggregateRequest, resourceRequestContext).then((response) => {
|
|
753
|
+
return mergeAggregateUiResponse(response, (first, second) => {
|
|
754
|
+
return mergeBatchRecordsFields(first, second, (a, b) => {
|
|
755
|
+
return mergeRelatedRecordsFields(a, b);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
-
/**
|
|
763
|
-
* Higher order function that accepts a network adapter and returns a new network adapter
|
|
764
|
-
* that is capable of performing field batching to ensure that URL length limits are respected
|
|
765
|
-
* when hitting record endpoints that accept a field list
|
|
766
|
-
*
|
|
767
|
-
* @param networkAdapter the network adapter to do the call.
|
|
768
|
-
*/
|
|
769
|
-
function makeNetworkAdapterChunkRecordFields(networkAdapter) {
|
|
770
|
-
// endpoint handlers that support aggregate-ui field batching
|
|
771
|
-
const batchHandlers = [
|
|
772
|
-
makeNetworkChunkFieldsGetRecord,
|
|
773
|
-
makeNetworkChunkFieldsGetRecordsBatch,
|
|
774
|
-
makeNetworkChunkFieldsGetRelatedListRecords,
|
|
775
|
-
makeNetworkChunkFieldsGetRelatedListRecordsBatch,
|
|
776
|
-
];
|
|
777
|
-
return batchHandlers.reduce((network, handler) => {
|
|
778
|
-
return handler(network);
|
|
779
|
-
}, networkAdapter);
|
|
762
|
+
/**
|
|
763
|
+
* Higher order function that accepts a network adapter and returns a new network adapter
|
|
764
|
+
* that is capable of performing field batching to ensure that URL length limits are respected
|
|
765
|
+
* when hitting record endpoints that accept a field list
|
|
766
|
+
*
|
|
767
|
+
* @param networkAdapter the network adapter to do the call.
|
|
768
|
+
*/
|
|
769
|
+
function makeNetworkAdapterChunkRecordFields(networkAdapter) {
|
|
770
|
+
// endpoint handlers that support aggregate-ui field batching
|
|
771
|
+
const batchHandlers = [
|
|
772
|
+
makeNetworkChunkFieldsGetRecord,
|
|
773
|
+
makeNetworkChunkFieldsGetRecordsBatch,
|
|
774
|
+
makeNetworkChunkFieldsGetRelatedListRecords,
|
|
775
|
+
makeNetworkChunkFieldsGetRelatedListRecordsBatch,
|
|
776
|
+
];
|
|
777
|
+
return batchHandlers.reduce((network, handler) => {
|
|
778
|
+
return handler(network);
|
|
779
|
+
}, networkAdapter);
|
|
780
780
|
}
|
|
781
781
|
|
|
782
782
|
export { NimbusNetworkAdapter, makeNetworkAdapterChunkRecordFields };
|