@salesforce/lds-runtime-mobile 1.228.1 → 1.229.0-dev10

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.
Files changed (3) hide show
  1. package/dist/main.js +490 -275
  2. package/package.json +18 -18
  3. package/sfdc/main.js +490 -275
package/dist/main.js CHANGED
@@ -17,6 +17,7 @@ import { HttpStatusCode, StoreKeySet, serializeStructuredKey, Reader, deepFreeze
17
17
  import excludeStaleRecordsGate from '@salesforce/gate/lds.graphqlEvalExcludeStaleRecords';
18
18
  import { parseAndVisit, Kind, visit, execute, buildSchema, isObjectType, defaultFieldResolver } from '@luvio/graphql-parser';
19
19
  import { getRecordId18, keyBuilderQuickActionExecutionRepresentation, ingestQuickActionExecutionRepresentation, keyBuilderContentDocumentCompositeRepresentation, getResponseCacheKeysContentDocumentCompositeRepresentation, keyBuilderFromTypeContentDocumentCompositeRepresentation, ingestContentDocumentCompositeRepresentation, keyBuilderRecord, getTypeCacheKeysRecord, keyBuilderFromTypeRecordRepresentation, ingestRecord, RecordRepresentationRepresentationType, ObjectInfoRepresentationType, getRecordAdapterFactory, getObjectInfoAdapterFactory, getObjectInfosAdapterFactory, UiApiNamespace, RecordRepresentationType, RecordRepresentationTTL, RecordRepresentationVersion, getRecordsAdapterFactory } from '@salesforce/lds-adapters-uiapi';
20
+ import ldsIdempotencyWriteDisabled from '@salesforce/gate/lds.idempotencyWriteDisabled';
20
21
  import caseSensitiveUserId from '@salesforce/user/Id';
21
22
  import { idleDetector, getInstrumentation } from 'o11y/client';
22
23
  import ldsUseShortUrlGate from '@salesforce/gate/lds.useShortUrl';
@@ -552,6 +553,9 @@ function handleDurableStoreRejection(instrument) {
552
553
  }
553
554
 
554
555
  function isStoreEntryError(storeRecord) {
556
+ if (!storeRecord || typeof storeRecord !== 'object') {
557
+ return false;
558
+ }
555
559
  return storeRecord.__type === 'error';
556
560
  }
557
561
 
@@ -1136,12 +1140,12 @@ function makeDurable(environment, { durableStore, instrumentation }) {
1136
1140
  }
1137
1141
  return environment.getNode(key, ingestStagingStore);
1138
1142
  };
1139
- const wrapNormalizedGraphNode = function (normalized) {
1143
+ const wrapNormalizedGraphNode = function (normalized, key) {
1140
1144
  validateNotDisposed();
1141
1145
  if (ingestStagingStore === null) {
1142
1146
  ingestStagingStore = buildIngestStagingStore(environment);
1143
1147
  }
1144
- return environment.wrapNormalizedGraphNode(normalized, ingestStagingStore);
1148
+ return environment.wrapNormalizedGraphNode(normalized, key, ingestStagingStore);
1145
1149
  };
1146
1150
  const rebuildSnapshot = function (snapshot, onRebuild) {
1147
1151
  validateNotDisposed();
@@ -1455,6 +1459,72 @@ function makeDurable(environment, { durableStore, instrumentation }) {
1455
1459
  });
1456
1460
  }
1457
1461
 
1462
+ /**
1463
+ * Copyright (c) 2022, Salesforce, Inc.,
1464
+ * All rights reserved.
1465
+ * For full license text, see the LICENSE.txt file
1466
+ */
1467
+
1468
+ const API_NAMESPACE = 'UiApi';
1469
+ const RECORD_REPRESENTATION_NAME = 'RecordRepresentation';
1470
+ const RECORD_VIEW_ENTITY_REPRESENTATION_NAME = 'RecordViewEntityRepresentation';
1471
+ const RECORD_ID_PREFIX = `${API_NAMESPACE}::${RECORD_REPRESENTATION_NAME}:`;
1472
+ const RECORD_VIEW_ENTITY_ID_PREFIX = `${API_NAMESPACE}::${RECORD_VIEW_ENTITY_REPRESENTATION_NAME}:Name:`;
1473
+ const RECORD_FIELDS_KEY_JUNCTION = '__fields__';
1474
+ function isStoreKeyRecordId(key) {
1475
+ return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
1476
+ }
1477
+ function isStoreKeyRecordViewEntity(key) {
1478
+ return (key.indexOf(RECORD_VIEW_ENTITY_ID_PREFIX) > -1 &&
1479
+ key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1);
1480
+ }
1481
+ function isStoreKeyRecordField(key) {
1482
+ return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) > -1;
1483
+ }
1484
+ function extractRecordIdFromStoreKey(key) {
1485
+ if (key === undefined ||
1486
+ (key.indexOf(RECORD_ID_PREFIX) === -1 && key.indexOf(RECORD_VIEW_ENTITY_ID_PREFIX) === -1)) {
1487
+ return undefined;
1488
+ }
1489
+ const parts = key.split(':');
1490
+ return parts[parts.length - 1].split('_')[0];
1491
+ }
1492
+ function buildRecordFieldStoreKey(recordKey, fieldName) {
1493
+ return `${recordKey}${RECORD_FIELDS_KEY_JUNCTION}${fieldName}`;
1494
+ }
1495
+ function objectsDeepEqual(lhs, rhs) {
1496
+ if (lhs === rhs)
1497
+ return true;
1498
+ if (typeof lhs !== 'object' || typeof rhs !== 'object' || lhs === null || rhs === null)
1499
+ return false;
1500
+ const lhsKeys = Object.keys(lhs);
1501
+ const rhsKeys = Object.keys(rhs);
1502
+ if (lhsKeys.length !== rhsKeys.length)
1503
+ return false;
1504
+ for (let key of lhsKeys) {
1505
+ if (!rhsKeys.includes(key))
1506
+ return false;
1507
+ if (typeof lhs[key] === 'function' || typeof rhs[key] === 'function') {
1508
+ if (lhs[key].toString() !== rhs[key].toString())
1509
+ return false;
1510
+ }
1511
+ else {
1512
+ if (!objectsDeepEqual(lhs[key], rhs[key]))
1513
+ return false;
1514
+ }
1515
+ }
1516
+ return true;
1517
+ }
1518
+
1519
+ function isStoreRecordError(storeRecord) {
1520
+ return storeRecord.__type === 'error';
1521
+ }
1522
+ function isEntryDurableRecordRepresentation(entry, key) {
1523
+ // Either a DurableRecordRepresentation or StoreRecordError can live at a record key
1524
+ return ((isStoreKeyRecordId(key) || isStoreKeyRecordViewEntity(key)) &&
1525
+ entry.data.__type === undefined);
1526
+ }
1527
+
1458
1528
  /**
1459
1529
  * Copyright (c) 2022, Salesforce, Inc.,
1460
1530
  * All rights reserved.
@@ -2714,6 +2784,7 @@ function isScalarDataType(type) {
2714
2784
  'Email',
2715
2785
  'TextArea',
2716
2786
  'Percent',
2787
+ 'EncryptedString',
2717
2788
  ].includes(type);
2718
2789
  }
2719
2790
 
@@ -4194,8 +4265,12 @@ function rootRecordQuery(selection, input) {
4194
4265
  // If there is no metadata for this query or it somehow lacks a timestamp
4195
4266
  // skip setting the root timestamp
4196
4267
  if (queryMetadata !== undefined && queryMetadata.ingestionTimestamp !== undefined) {
4197
- // subtract 10ms from timestamp to account for ingestion processing time
4198
- input.rootTimestamp = queryMetadata.ingestionTimestamp - 10;
4268
+ const timestamp = Number(queryMetadata.ingestionTimestamp);
4269
+ if (!isNaN(timestamp)) {
4270
+ // adjust the timestamp to account for ingestion processing time
4271
+ // 30s is used because this is the default record TTL
4272
+ input.rootTimestamp = timestamp - 30000;
4273
+ }
4199
4274
  }
4200
4275
  }
4201
4276
  return recordQuery(selection, alias, apiName, [], input);
@@ -4653,7 +4728,11 @@ function makeStoreEval(preconditioner, objectInfoService, userId, contextProvide
4653
4728
  try {
4654
4729
  const { data, seenRecords } = await queryEvaluator(rootQuery, context, eventEmitter);
4655
4730
  const rebuildWithStoreEval = ((originalSnapshot) => {
4656
- return storeEval(config, originalSnapshot, observers, connectionKeyBuilder);
4731
+ return storeEval(config, originalSnapshot, observers, connectionKeyBuilder).then((rebuiltSnapshot) => {
4732
+ return objectsDeepEqual(originalSnapshot.data, rebuiltSnapshot.data)
4733
+ ? originalSnapshot
4734
+ : rebuiltSnapshot;
4735
+ });
4657
4736
  });
4658
4737
  const recordId = generateUniqueRecordId$1();
4659
4738
  // if the non-eval'ed snapshot was an error then we return a synthetic
@@ -5004,8 +5083,12 @@ function uuidv4() {
5004
5083
 
5005
5084
  const HTTP_HEADER_RETRY_AFTER = 'Retry-After';
5006
5085
  const HTTP_HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';
5086
+ const ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED = 'IDEMPOTENCY_FEATURE_NOT_ENABLED';
5087
+ const ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED = 'IDEMPOTENCY_NOT_SUPPORTED';
5007
5088
  const ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER = 'IDEMPOTENCY_KEY_USED_DIFFERENT_USER';
5008
5089
  const ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST = 'IDEMPOTENCY_CONCURRENT_REQUEST';
5090
+ const ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED = 'IDEMPOTENCY_KEY_ALREADY_USED';
5091
+ const ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR = 'IDEMPOTENCY_BACKEND_OPERATION_ERROR';
5009
5092
  /**
5010
5093
  * Get the retry after in milliseconds from the response headers, undefined if not specified.
5011
5094
  * The header could have two different format.
@@ -5035,7 +5118,9 @@ function buildLuvioOverrideForDraftAdapters(luvio, handler, extractTargetIdFromC
5035
5118
  const dispatchResourceRequest = async function (resourceRequest, _context) {
5036
5119
  const resourceRequestCopy = clone$1(resourceRequest);
5037
5120
  resourceRequestCopy.headers = resourceRequestCopy.headers || {};
5038
- resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
5121
+ if (handler.hasIdempotencySupport()) {
5122
+ resourceRequestCopy.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
5123
+ }
5039
5124
  // enable return extra fields for record creation and record update http call
5040
5125
  if (resourceRequest.basePath === '/ui-api/records' &&
5041
5126
  (resourceRequest.method === 'post' || resourceRequest.method === 'patch')) {
@@ -5871,6 +5956,12 @@ class AbstractResourceRequestActionHandler {
5871
5956
  // the luvio store redirect table, during which a new draft might be enqueued
5872
5957
  // which would not see a necessary mapping.
5873
5958
  this.ephemeralRedirects = {};
5959
+ // determined by Server setup.
5960
+ this.isIdempotencySupported = true;
5961
+ // idempotency write flag set by lds
5962
+ this.isLdsIdempotencyWriteDisabled = ldsIdempotencyWriteDisabled.isOpen({
5963
+ fallback: false,
5964
+ });
5874
5965
  }
5875
5966
  enqueue(data) {
5876
5967
  return this.draftQueue.enqueue(this.handlerId, data);
@@ -5900,21 +5991,43 @@ class AbstractResourceRequestActionHandler {
5900
5991
  retryDelayInMs = getRetryAfterInMs(response.headers);
5901
5992
  shouldRetry = true;
5902
5993
  break;
5903
- case HttpStatusCode.ServerError:
5994
+ case HttpStatusCode.ServerError: {
5904
5995
  shouldRetry = true;
5996
+ if (this.handleIdempotencyServerError(response.body, updatedAction, false, ERROR_CODE_IDEMPOTENCY_BACKEND_OPERATION_ERROR)) {
5997
+ this.isIdempotencySupported = false;
5998
+ retryDelayInMs = 0;
5999
+ actionDataChanged = true;
6000
+ }
5905
6001
  break;
6002
+ }
5906
6003
  case 409 /* IdempotentWriteSpecificHttpStatusCode.Conflict */: {
5907
- const errorCode = response.body[0].errorCode;
5908
- if (errorCode === ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER) {
5909
- updatedAction.data.headers = updatedAction.data.headers || {};
5910
- updatedAction.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
6004
+ if (this.isUiApiErrors(response.body)) {
6005
+ const errorCode = response.body[0].errorCode;
6006
+ if (this.handleIdempotencyServerError(response.body, updatedAction, true, ERROR_CODE_IDEMPOTENCY_KEY_USED_DIFFERENT_USER)) {
6007
+ retryDelayInMs = 0;
6008
+ actionDataChanged = true;
6009
+ }
6010
+ else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
6011
+ retryDelayInMs = getRetryAfterInMs(response.headers);
6012
+ }
6013
+ shouldRetry = true;
6014
+ }
6015
+ break;
6016
+ }
6017
+ case HttpStatusCode.BadRequest: {
6018
+ if (this.handleIdempotencyServerError(response.body, updatedAction, false, ERROR_CODE_IDEMPOTENCY_FEATURE_NOT_ENABLED, ERROR_CODE_IDEMPOTENCY_NOT_SUPPORTED)) {
5911
6019
  retryDelayInMs = 0;
5912
6020
  actionDataChanged = true;
6021
+ shouldRetry = true;
5913
6022
  }
5914
- else if (errorCode === ERROR_CODE_IDEMPOTENCY_CONCURRENT_REQUEST) {
5915
- retryDelayInMs = getRetryAfterInMs(response.headers);
6023
+ break;
6024
+ }
6025
+ case 422 /* IdempotentWriteSpecificHttpStatusCode.UnProcessableEntity */: {
6026
+ if (this.handleIdempotencyServerError(response.body, updatedAction, true, ERROR_CODE_IDEMPOTENCY_KEY_ALREADY_USED)) {
6027
+ retryDelayInMs = 0;
6028
+ actionDataChanged = true;
6029
+ shouldRetry = true;
5916
6030
  }
5917
- shouldRetry = true;
5918
6031
  break;
5919
6032
  }
5920
6033
  }
@@ -5933,6 +6046,27 @@ class AbstractResourceRequestActionHandler {
5933
6046
  return ProcessActionResult.NETWORK_ERROR;
5934
6047
  }
5935
6048
  }
6049
+ // true if response is an idempotency server error. updates or deletes idempotency key if the reponse is idempotency related error. Idempotency related error is in format of UiApiError array.
6050
+ handleIdempotencyServerError(responseBody, action, updateIdempotencyKey, ...targetErrorCodes) {
6051
+ if (this.isUiApiErrors(responseBody)) {
6052
+ const errorCode = responseBody[0].errorCode;
6053
+ if (targetErrorCodes.includes(errorCode)) {
6054
+ action.data.headers = action.data.headers || {};
6055
+ if (updateIdempotencyKey) {
6056
+ action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
6057
+ }
6058
+ else {
6059
+ delete action.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY];
6060
+ }
6061
+ return true;
6062
+ }
6063
+ }
6064
+ return false;
6065
+ }
6066
+ // checks if the body is an array of UiApiError. Sometimes the body has `enhancedErrorType` field as an error indicator(one example is the field validation failure). In such case Action being processed updates to an Error Action.
6067
+ isUiApiErrors(body) {
6068
+ return body !== undefined && Array.isArray(body) && body.length > 0 && body[0].errorCode;
6069
+ }
5936
6070
  async buildPendingAction(request, queue) {
5937
6071
  const targetId = await this.getIdFromRequest(request);
5938
6072
  if (targetId === undefined) {
@@ -6146,6 +6280,10 @@ class AbstractResourceRequestActionHandler {
6146
6280
  ...targetData,
6147
6281
  body: this.mergeRequestBody(targetBody, sourceBody),
6148
6282
  };
6283
+ // Updates Idempotency key if target has one
6284
+ if (targetData.headers && targetData.headers[HTTP_HEADER_IDEMPOTENCY_KEY]) {
6285
+ merged.data.headers[HTTP_HEADER_IDEMPOTENCY_KEY] = uuidv4();
6286
+ }
6149
6287
  // overlay metadata
6150
6288
  merged.metadata = { ...targetMetadata, ...sourceMetadata };
6151
6289
  // put status back to pending to auto upload if queue is active and targed is at the head.
@@ -6182,6 +6320,9 @@ class AbstractResourceRequestActionHandler {
6182
6320
  getDraftIdsFromAction(action) {
6183
6321
  return [action.targetId];
6184
6322
  }
6323
+ hasIdempotencySupport() {
6324
+ return this.isIdempotencySupported && !this.isLdsIdempotencyWriteDisabled;
6325
+ }
6185
6326
  async ingestResponses(responses, action) {
6186
6327
  const luvio = this.getLuvio();
6187
6328
  await luvio.handleSuccessResponse(() => {
@@ -6613,49 +6754,6 @@ function makeEnvironmentDraftAware(luvio, env, durableStore, handlers, draftQueu
6613
6754
  });
6614
6755
  }
6615
6756
 
6616
- /**
6617
- * Copyright (c) 2022, Salesforce, Inc.,
6618
- * All rights reserved.
6619
- * For full license text, see the LICENSE.txt file
6620
- */
6621
-
6622
- const API_NAMESPACE = 'UiApi';
6623
- const RECORD_REPRESENTATION_NAME = 'RecordRepresentation';
6624
- const RECORD_VIEW_ENTITY_REPRESENTATION_NAME = 'RecordViewEntityRepresentation';
6625
- const RECORD_ID_PREFIX = `${API_NAMESPACE}::${RECORD_REPRESENTATION_NAME}:`;
6626
- const RECORD_VIEW_ENTITY_ID_PREFIX = `${API_NAMESPACE}::${RECORD_VIEW_ENTITY_REPRESENTATION_NAME}:Name:`;
6627
- const RECORD_FIELDS_KEY_JUNCTION = '__fields__';
6628
- function isStoreKeyRecordId(key) {
6629
- return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
6630
- }
6631
- function isStoreKeyRecordViewEntity(key) {
6632
- return (key.indexOf(RECORD_VIEW_ENTITY_ID_PREFIX) > -1 &&
6633
- key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1);
6634
- }
6635
- function isStoreKeyRecordField(key) {
6636
- return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) > -1;
6637
- }
6638
- function extractRecordIdFromStoreKey(key) {
6639
- if (key === undefined ||
6640
- (key.indexOf(RECORD_ID_PREFIX) === -1 && key.indexOf(RECORD_VIEW_ENTITY_ID_PREFIX) === -1)) {
6641
- return undefined;
6642
- }
6643
- const parts = key.split(':');
6644
- return parts[parts.length - 1].split('_')[0];
6645
- }
6646
- function buildRecordFieldStoreKey(recordKey, fieldName) {
6647
- return `${recordKey}${RECORD_FIELDS_KEY_JUNCTION}${fieldName}`;
6648
- }
6649
-
6650
- function isStoreRecordError(storeRecord) {
6651
- return storeRecord.__type === 'error';
6652
- }
6653
- function isEntryDurableRecordRepresentation(entry, key) {
6654
- // Either a DurableRecordRepresentation or StoreRecordError can live at a record key
6655
- return ((isStoreKeyRecordId(key) || isStoreKeyRecordViewEntity(key)) &&
6656
- entry.data.__type === undefined);
6657
- }
6658
-
6659
6757
  function serializeFieldArguments(argumentNodes, variables) {
6660
6758
  const mutableArgumentNodes = Object.assign([], argumentNodes);
6661
6759
  return `args__(${mutableArgumentNodes
@@ -8030,6 +8128,10 @@ function isOperationDefinitionNode(node) {
8030
8128
  return node.kind === 'OperationDefinition';
8031
8129
  }
8032
8130
 
8131
+ const POLYMORPHIC_PARENT_RELATIONSHIP = 'polymorphicParentRelationship';
8132
+ const PARENT_RELATIONSHIP = 'parentRelationship';
8133
+ const CHILD_RELATIONSHIP = 'childRelationship';
8134
+ const RECORD_QUERY = 'recordQuery';
8033
8135
  function requestsDraftsField(recordFieldNode) {
8034
8136
  if (!recordFieldNode.selectionSet)
8035
8137
  return false;
@@ -8045,18 +8147,41 @@ function isRecordQuery(recordQueryField) {
8045
8147
  directive.arguments
8046
8148
  .map((argument) => argument.value)
8047
8149
  .filter(isStringValueNode)
8048
- .some((categoryName) => categoryName.value === 'recordQuery'));
8150
+ .some((categoryName) => categoryName.value === RECORD_QUERY));
8049
8151
  });
8050
8152
  }
8051
8153
  return false;
8052
8154
  }
8053
- // finds field with 'recordQuery' and 'childRelationship' directive
8054
- function findNearestRecordQuery(ancestors) {
8055
- const recordQueryAncester = findNearestAncesterPath(ancestors, true).node;
8056
- return recordQueryAncester === undefined ? undefined : recordQueryAncester;
8155
+ // finds connection field with 'recordQuery' and 'childRelationship' directive.
8156
+ function findNearestConnection(ancestors) {
8157
+ const connectionAncestor = findNearestAncesterPath(ancestors, true).node;
8158
+ return connectionAncestor === undefined ? undefined : connectionAncestor;
8159
+ }
8160
+ // convinient method to find nearest connection with its path
8161
+ function findNearestConnectionWithPath(ancestors) {
8162
+ const closestAncestorPath = findNearestAncesterPath(ancestors, true);
8163
+ let connection = undefined;
8164
+ let connectionPath = undefined;
8165
+ if (closestAncestorPath.parentIndex > 0) {
8166
+ const connectionAncestor = closestAncestorPath.node;
8167
+ const connectionAncestors = ancestors.slice(0, closestAncestorPath.parentIndex);
8168
+ connection =
8169
+ connectionAncestor === undefined ? undefined : connectionAncestor;
8170
+ if (connection !== undefined) {
8171
+ const ancesterPath = findAncesterPath(connectionAncestors);
8172
+ connectionPath =
8173
+ ancesterPath === ''
8174
+ ? connection.name.value
8175
+ : `${ancesterPath}#${connection.name.value}`;
8176
+ }
8177
+ }
8178
+ return {
8179
+ connection,
8180
+ path: connectionPath,
8181
+ };
8057
8182
  }
8058
- // finds cloeset ancester. If 'parentRelationship' is allowed, it could be 'InlineFragmentNode' since it inherits the 'parent' relationship. 'InlineFragmentNode' makes sure that only one 'apiName' returns when tree is traversed.
8059
- function findNearestAncesterPath(ancestors, recordQueryOnly) {
8183
+ // finds closest ancestor. If node with 'parentRelationship' is the ancester, the end result could be 'InlineFragmentNode' since it inherits the 'parent' relationship. 'InlineFragmentNode' makes sure that only one 'apiName' returns when tree is traversed.
8184
+ function findNearestAncesterPath(ancestors, connectionOnly) {
8060
8185
  let recordQueryPath = { node: undefined, parentIndex: -1 };
8061
8186
  let relationship = '';
8062
8187
  for (let i = ancestors.length - 1; i >= 0; i--) {
@@ -8070,9 +8195,11 @@ function findNearestAncesterPath(ancestors, recordQueryOnly) {
8070
8195
  continue;
8071
8196
  for (let arg of directive.arguments) {
8072
8197
  if (arg.value &&
8073
- (arg.value.value === 'recordQuery' ||
8074
- arg.value.value === 'childRelationship' ||
8075
- (!recordQueryOnly && arg.value.value === 'parentRelationship'))) {
8198
+ (arg.value.value === RECORD_QUERY ||
8199
+ arg.value.value === CHILD_RELATIONSHIP ||
8200
+ (!connectionOnly &&
8201
+ (arg.value.value === PARENT_RELATIONSHIP ||
8202
+ arg.value.value === POLYMORPHIC_PARENT_RELATIONSHIP)))) {
8076
8203
  recordQueryPath = { node: node, parentIndex: i };
8077
8204
  relationship = arg.value.value;
8078
8205
  break;
@@ -8087,17 +8214,19 @@ function findNearestAncesterPath(ancestors, recordQueryOnly) {
8087
8214
  //checks if nearest ancester could be an inline fragment
8088
8215
  if (recordQueryPath.node !== undefined &&
8089
8216
  recordQueryPath.node.selectionSet &&
8090
- relationship === 'parentRelationship') {
8091
- //
8092
- if (recordQueryPath.node.selectionSet.selections.every(isInlineFragmentNode)) {
8093
- //
8094
- const inlineFragmentLoc = recordQueryPath.parentIndex + 2;
8095
- if (inlineFragmentLoc < ancestors.length && ancestors[inlineFragmentLoc]) {
8217
+ (relationship === PARENT_RELATIONSHIP || relationship === POLYMORPHIC_PARENT_RELATIONSHIP)) {
8218
+ // InlineFragment is usually 3 steps aways from its FieldNode parent within ancester hierarchy if it exists. The below search
8219
+ // is applied to adapt to future AST structure change
8220
+ let parentIndex = recordQueryPath.parentIndex + 1;
8221
+ while (parentIndex < ancestors.length) {
8222
+ if (isInlineFragmentNode(ancestors[parentIndex])) {
8096
8223
  recordQueryPath = {
8097
- node: ancestors[inlineFragmentLoc],
8098
- parentIndex: inlineFragmentLoc,
8224
+ node: ancestors[parentIndex],
8225
+ parentIndex,
8099
8226
  };
8227
+ break;
8100
8228
  }
8229
+ parentIndex++;
8101
8230
  }
8102
8231
  }
8103
8232
  return recordQueryPath;
@@ -8121,7 +8250,7 @@ function findAncesterPath(ancesters) {
8121
8250
  ? sectionPath
8122
8251
  : sectionPath === ''
8123
8252
  ? path
8124
- : `${sectionPath}_${path}`;
8253
+ : `${sectionPath}#${path}`;
8125
8254
  }
8126
8255
  }
8127
8256
  boundaryIndex = parentIndex;
@@ -8179,9 +8308,9 @@ function getRelation(node) {
8179
8308
  const relationships = args
8180
8309
  .map((arg) => arg.value)
8181
8310
  .filter(isStringValueNode)
8182
- .filter((valueNode) => valueNode.value === 'childRelationship' ||
8183
- valueNode.value === 'parentRelationship' ||
8184
- valueNode.value === 'polymorphicParentRelationship')
8311
+ .filter((valueNode) => valueNode.value === CHILD_RELATIONSHIP ||
8312
+ valueNode.value === PARENT_RELATIONSHIP ||
8313
+ valueNode.value === POLYMORPHIC_PARENT_RELATIONSHIP)
8185
8314
  .map((relationshipNode) => relationshipNode.value);
8186
8315
  if (relationships.length > 0) {
8187
8316
  return relationships[0];
@@ -8238,8 +8367,8 @@ function isFieldSpanning(node, parentNode) {
8238
8367
  */
8239
8368
  function isParentRelationship(node) {
8240
8369
  return (node &&
8241
- (isRelationship(node, 'parentRelationship') ||
8242
- isRelationship(node, 'polymorphicParentRelationship')));
8370
+ (isRelationship(node, PARENT_RELATIONSHIP) ||
8371
+ isRelationship(node, POLYMORPHIC_PARENT_RELATIONSHIP)));
8243
8372
  }
8244
8373
  /*
8245
8374
  checks if the InlineFragment spans
@@ -8545,6 +8674,26 @@ function findFieldInfo(objectInfo, fieldName) {
8545
8674
  return values$1(objectInfo.fields).find((field) => field.apiName === fieldName ||
8546
8675
  (field.dataType === 'Reference' && field.relationshipName === fieldName));
8547
8676
  }
8677
+ async function readIngestionTimestampForKey(key, query) {
8678
+ let ingestionTimestamp = 0;
8679
+ const sql = `
8680
+ SELECT json_extract(metadata, '${JSON_EXTRACT_PATH_INGESTION_TIMESTAMP}')
8681
+ FROM lds_data
8682
+ WHERE key IS ?
8683
+ `;
8684
+ const results = await query(sql, [key]);
8685
+ const [timestamp] = results.rows.map((row) => row[0]);
8686
+ if (timestamp !== null) {
8687
+ const numericalTimestamp = Number(timestamp);
8688
+ if (isNaN(numericalTimestamp)) {
8689
+ return ingestionTimestamp;
8690
+ }
8691
+ // adjust the timestamp to account for ingestion processing time
8692
+ // 30s is used because this is the default record TTL
8693
+ ingestionTimestamp = numericalTimestamp - 30000;
8694
+ }
8695
+ return ingestionTimestamp;
8696
+ }
8548
8697
 
8549
8698
  function findSpanningField(name) {
8550
8699
  return (field) => {
@@ -9064,17 +9213,7 @@ async function fetchIngestionTimeStampFromDatabase(apiName, info, args, query) {
9064
9213
  const key = buildKeyStringForRecordQuery(operation,
9065
9214
  // join varables passed from query to the argument variables given from the AST
9066
9215
  { ...variableValues, ...args }, info.fieldNodes[0].arguments, apiName);
9067
- const sql = `
9068
- SELECT json_extract(metadata, '${JSON_EXTRACT_PATH_INGESTION_TIMESTAMP}')
9069
- FROM lds_data
9070
- WHERE key IS ?
9071
- `;
9072
- const results = await query(sql, [key]);
9073
- const [timestamp] = results.rows.map((row) => row[0]);
9074
- if (timestamp !== null && typeof timestamp === 'number') {
9075
- //go back 10 ms to adjust for margin of error when top level query is stored and when raml objects are stored
9076
- ingestionTimestamp = timestamp - 10;
9077
- }
9216
+ return readIngestionTimestampForKey(key, query);
9078
9217
  }
9079
9218
  return ingestionTimestamp;
9080
9219
  }
@@ -9122,26 +9261,20 @@ function generateRecordQueries(objectInfos) {
9122
9261
  let recordConnections = ``;
9123
9262
  const polymorphicFieldTypeNames = new Set();
9124
9263
  let typedScalars = new Set();
9264
+ let parentRelationshipFields = new Set();
9125
9265
  for (const objectInfo of values$1(objectInfos)) {
9126
9266
  const { apiName, childRelationships } = objectInfo;
9127
9267
  let fields = ``;
9128
9268
  typedScalars.add(`${apiName}_Filter`);
9129
9269
  typedScalars.add(`${apiName}_OrderBy`);
9130
- for (const childRelationship of childRelationships) {
9131
- const { childObjectApiName } = childRelationship;
9132
- // Only add the relationship if there is relevant objectinfos for it,
9133
- // otherwise we'd be defining types we cannot satisfy and aren't referenced in
9134
- // the query.
9135
- if (objectInfos[childObjectApiName] !== undefined) {
9136
- fields += `${childRelationship.relationshipName}(first: Int, where: ${childObjectApiName}_Filter, orderBy: ${childObjectApiName}_OrderBy, scope: SupportedScopes): ${childObjectApiName}Connection \n`;
9137
- typedScalars.add(`${childObjectApiName}_Filter`);
9138
- typedScalars.add(`${childObjectApiName}_OrderBy`);
9139
- }
9140
- }
9141
9270
  for (const field of values$1(objectInfo.fields)) {
9142
9271
  if (!fieldsStaticallyAdded.includes(field.apiName)) {
9143
9272
  fields += `${field.apiName}: ${dataTypeToType(field.dataType, field.apiName)}\n`;
9144
9273
  }
9274
+ //handles parent relationship
9275
+ if (field.relationshipName === null) {
9276
+ continue;
9277
+ }
9145
9278
  // For spanning parent relationships with no union types
9146
9279
  if (field.referenceToInfos.length === 1) {
9147
9280
  const [relation] = field.referenceToInfos;
@@ -9149,11 +9282,13 @@ function generateRecordQueries(objectInfos) {
9149
9282
  // otherwise we'd be defining types we cannot satisfy and aren't referenced in
9150
9283
  // the query.
9151
9284
  if (objectInfos[relation.apiName] !== undefined) {
9285
+ parentRelationshipFields.add(field.relationshipName);
9152
9286
  fields += `${field.relationshipName}: ${relation.apiName}\n`;
9153
9287
  }
9154
9288
  // For polymorphic field, its type is 'Record' inteface. The concrete entity type name is saved for field resolving of next phase
9155
9289
  }
9156
9290
  else if (field.referenceToInfos.length > 1) {
9291
+ parentRelationshipFields.add(field.relationshipName);
9157
9292
  fields += `${field.relationshipName}: Record\n`;
9158
9293
  for (const relation of field.referenceToInfos) {
9159
9294
  if (objectInfos[relation.apiName] !== undefined) {
@@ -9162,6 +9297,20 @@ function generateRecordQueries(objectInfos) {
9162
9297
  }
9163
9298
  }
9164
9299
  }
9300
+ // handles child relationship
9301
+ for (const childRelationship of childRelationships) {
9302
+ const { childObjectApiName } = childRelationship;
9303
+ // Only add the relationship if there is relevant objectinfos for it,
9304
+ // otherwise we'd be defining types we cannot satisfy and aren't referenced in
9305
+ // the query.
9306
+ // If one field has both parent relationship and child relationship with the same name, the child relationship is ignored. This is how the server GQL has implemented as date of 08/07/2023
9307
+ if (objectInfos[childObjectApiName] !== undefined &&
9308
+ !parentRelationshipFields.has(childRelationship.relationshipName)) {
9309
+ fields += `${childRelationship.relationshipName}(first: Int, where: ${childObjectApiName}_Filter, orderBy: ${childObjectApiName}_OrderBy, scope: SupportedScopes): ${childObjectApiName}Connection \n`;
9310
+ typedScalars.add(`${childObjectApiName}_Filter`);
9311
+ typedScalars.add(`${childObjectApiName}_OrderBy`);
9312
+ }
9313
+ }
9165
9314
  recordQueries += `${apiName}(first: Int, where: ${apiName}_Filter, orderBy: ${apiName}_OrderBy, scope: SupportedScopes): ${apiName}Connection\n`;
9166
9315
  const isServiceAppointment = apiName === 'ServiceAppointment';
9167
9316
  recordConnections += /* GraphQL */ `
@@ -9236,6 +9385,8 @@ function dataTypeToType(objectInfoDataType, apiName) {
9236
9385
  return 'PercentValue';
9237
9386
  case 'Int':
9238
9387
  return 'IntValue';
9388
+ case 'EncryptedString':
9389
+ return 'EncryptedStringValue';
9239
9390
  // ! do the rest of the custom types
9240
9391
  default:
9241
9392
  return 'String';
@@ -9321,7 +9472,7 @@ const parentRelationshipDirective = {
9321
9472
  },
9322
9473
  value: {
9323
9474
  kind: Kind.STRING,
9324
- value: 'parentRelationship',
9475
+ value: PARENT_RELATIONSHIP,
9325
9476
  block: false,
9326
9477
  },
9327
9478
  },
@@ -9335,8 +9486,8 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9335
9486
  // example 2 'ServiceAppointment' -> ['Owner']; 'Owner' -> ['User', 'Group']
9336
9487
  const objectNodeInfoTree = {};
9337
9488
  // save the field path to apiName map
9338
- // example 1: 'ServiceAppointment' -> ['ServiceAppointment']; 'ServiceAppointment_Account' -> ['Account']; 'ServiceAppointment_Account_Owner' -> ['User']
9339
- const objectInfoApiMap = {};
9489
+ // example 1: 'ServiceAppointment' -> ['ServiceAppointment']; 'ServiceAppointment#ccount' -> ['Account']; 'ServiceAppointment#Account#Owner' -> ['User']
9490
+ const pathToObjectApiNamesMap = {};
9340
9491
  let startNodes = new Set();
9341
9492
  let totalNodes = new Set();
9342
9493
  let objectInfos = {};
@@ -9350,11 +9501,11 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9350
9501
  visit(originalAST, {
9351
9502
  Argument: {
9352
9503
  enter(node, key, parent, path, ancestors) {
9353
- const recordQueryNode = findNearestRecordQuery(ancestors);
9354
- if (!recordQueryNode)
9504
+ const { connection: recordConnectionNode, path: ancesterPath } = findNearestConnectionWithPath(ancestors);
9505
+ if (!recordConnectionNode || !ancesterPath)
9355
9506
  return;
9356
- if (!objectNodeInfoTree[recordQueryNode.name.value]) {
9357
- objectNodeInfoTree[recordQueryNode.name.value] = [];
9507
+ if (!objectNodeInfoTree[ancesterPath]) {
9508
+ objectNodeInfoTree[ancesterPath] = [];
9358
9509
  }
9359
9510
  switch (node.name.value) {
9360
9511
  case 'orderBy':
@@ -9362,12 +9513,12 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9362
9513
  if (node.value.kind !== 'ObjectValue') {
9363
9514
  return;
9364
9515
  }
9365
- totalNodes.add(recordQueryNode.name.value);
9516
+ totalNodes.add(ancesterPath);
9366
9517
  // 'childRelationship' node is not taken as the startNode of the 'NodeInfoTree' graph. The field scanning will construct the graph which lead here.
9367
- if (isRecordQuery(recordQueryNode)) {
9368
- startNodes.add(recordQueryNode.name.value);
9518
+ if (isRecordQuery(recordConnectionNode)) {
9519
+ startNodes.add(recordConnectionNode.name.value);
9369
9520
  }
9370
- growObjectFieldTree(objectNodeInfoTree, recordQueryNode.name.value, node.value, totalNodes, startNodes);
9521
+ growObjectFieldTree(objectNodeInfoTree, ancesterPath, node.value, totalNodes, startNodes);
9371
9522
  break;
9372
9523
  case 'scope':
9373
9524
  if (!isScopeArgumentNodeWithType(node, 'ASSIGNEDTOME', variables)) {
@@ -9382,17 +9533,16 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9382
9533
  name: 'ServiceResources',
9383
9534
  });
9384
9535
  }
9385
- if (objectNodeInfoTree['ServiceResources'] === undefined) {
9386
- objectNodeInfoTree['ServiceResources'] = [];
9387
- }
9388
- if (!objectNodeInfoTree['ServiceResources'].some((child) => child.name === 'ServiceResource')) {
9389
- objectNodeInfoTree['ServiceResources'].push({
9390
- relation: 'parent',
9391
- name: 'ServiceResource',
9392
- });
9536
+ if (objectNodeInfoTree['ServiceAppointment#ServiceResources'] === undefined) {
9537
+ objectNodeInfoTree['ServiceAppointment#ServiceResources'] = [
9538
+ {
9539
+ relation: 'parent',
9540
+ name: 'ServiceResource',
9541
+ },
9542
+ ];
9393
9543
  }
9394
- if (objectNodeInfoTree['ServiceResource'] === undefined) {
9395
- objectNodeInfoTree['ServiceResource'] = [];
9544
+ if (objectNodeInfoTree['ServiceAppointment#ServiceResources#ServiceResource'] === undefined) {
9545
+ objectNodeInfoTree['ServiceAppointment#ServiceResources#ServiceResource'] = [];
9396
9546
  }
9397
9547
  break;
9398
9548
  default:
@@ -9406,7 +9556,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9406
9556
  return;
9407
9557
  if (!node.selectionSet)
9408
9558
  return;
9409
- const recordQueryField = findNearestRecordQuery(ancestors);
9559
+ const recordQueryField = findNearestConnection(ancestors);
9410
9560
  //only injects fields for 'recordQuery' field. ignores the 'childRelationship' field since it will be traversed as the child of the 'recordQuery'
9411
9561
  if (isRecordQuery(recordQueryField) && recordQueryField) {
9412
9562
  totalNodes.add(recordQueryField.name.value);
@@ -9417,21 +9567,21 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9417
9567
  },
9418
9568
  });
9419
9569
  if (objectInfoService && startNodes.size > 0) {
9420
- objectInfos = await resolveObjectInfos(objectNodeInfoTree, objectInfoApiMap, startNodes, objectInfoService);
9570
+ objectInfos = await resolveObjectInfos(objectNodeInfoTree, pathToObjectApiNamesMap, startNodes, objectInfoService);
9421
9571
  }
9422
9572
  // read pass; gather whats needed
9423
9573
  visit(originalAST, {
9424
9574
  Argument: {
9425
9575
  leave(node, key, parent, path, ancestors) {
9426
- const recordQueryField = findNearestRecordQuery(ancestors);
9576
+ const recordQueryField = findNearestConnection(ancestors);
9427
9577
  if (!recordQueryField)
9428
9578
  return;
9429
9579
  const ancestorPath = findAncesterPath(ancestors);
9430
9580
  if (!inlineFragmentSelections[ancestorPath]) {
9431
9581
  inlineFragmentSelections[ancestorPath] = [];
9432
9582
  }
9433
- const recordQueryApiName = objectInfoApiMap[ancestorPath]
9434
- ? objectInfoApiMap[ancestorPath][0]
9583
+ const recordQueryApiName = pathToObjectApiNamesMap[ancestorPath]
9584
+ ? pathToObjectApiNamesMap[ancestorPath][0]
9435
9585
  : recordQueryField.name.value;
9436
9586
  // The record node acts as the reference. The duplicated field in the record node is not injected
9437
9587
  const recordReferenceNode = [recordQueryField]
@@ -9445,7 +9595,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9445
9595
  case 'scope':
9446
9596
  // Hanle 'MINE' field
9447
9597
  if (isScopeArgumentNodeWithType(node, 'MINE', variables)) {
9448
- if (isMineScopeAvailable(ancestorPath, objectInfoApiMap, objectInfos)) {
9598
+ if (isMineScopeAvailable(ancestorPath, pathToObjectApiNamesMap, objectInfos)) {
9449
9599
  // 'typeConditon' is added when the 'InlineFragmentNode' is appended at the write pass
9450
9600
  inlineFragmentSelections[ancestorPath].push(...mineFragmentSelections);
9451
9601
  }
@@ -9471,7 +9621,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9471
9621
  case 'where': {
9472
9622
  inlineFragmentSelections[ancestorPath] = [
9473
9623
  ...inlineFragmentSelections[ancestorPath],
9474
- ...injectFilter(node, idState, ancestorPath, objectInfos, objectInfoApiMap, draftFunctions, recordReferenceNode),
9624
+ ...injectFilter(node, idState, ancestorPath, false, objectInfos, pathToObjectApiNamesMap, draftFunctions, recordReferenceNode),
9475
9625
  ];
9476
9626
  break;
9477
9627
  }
@@ -9487,7 +9637,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9487
9637
  if (!node.selectionSet)
9488
9638
  return;
9489
9639
  // it could be 'recordQuery' or 'childRelationship'
9490
- const recordQueryField = findNearestRecordQuery(ancestors);
9640
+ const recordQueryField = findNearestConnection(ancestors);
9491
9641
  if (!recordQueryField)
9492
9642
  return;
9493
9643
  const ancestorPath = findAncesterPath(ancestors);
@@ -9499,7 +9649,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9499
9649
  spanningSelections.push(selection);
9500
9650
  }
9501
9651
  }
9502
- const injectedFields = injectFields(spanningSelections, node, ancestors, objectInfos, objectInfoApiMap);
9652
+ const injectedFields = injectFields(spanningSelections, node, ancestorPath, ancestors, objectInfos, pathToObjectApiNamesMap);
9503
9653
  const mergedInjectedFields = mergeSelectionNodes(inlineFragmentSelections[ancestorPath], injectedFields);
9504
9654
  inlineFragmentSelections[ancestorPath] = mergedInjectedFields;
9505
9655
  },
@@ -9512,7 +9662,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9512
9662
  // removes 'ServicesResources' query field node if 'assignedtome' scope shows up
9513
9663
  if (assignedtomeQueryFieldNode !== undefined &&
9514
9664
  node.name.value === 'ServiceResources') {
9515
- const serviceResourcesAncestor = findNearestRecordQuery(ancestors);
9665
+ const serviceResourcesAncestor = findNearestConnection(ancestors);
9516
9666
  if (serviceResourcesAncestor === assignedtomeQueryFieldNode) {
9517
9667
  return null;
9518
9668
  }
@@ -9521,7 +9671,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9521
9671
  return;
9522
9672
  if (!node.selectionSet)
9523
9673
  return;
9524
- const recordQueryField = findNearestRecordQuery(ancestors);
9674
+ const recordQueryField = findNearestConnection(ancestors);
9525
9675
  if (!recordQueryField)
9526
9676
  return;
9527
9677
  const ancestorPath = findAncesterPath(ancestors);
@@ -9530,8 +9680,8 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9530
9680
  return;
9531
9681
  //const recordQueryPath = findAncesterPath(ancestors);
9532
9682
  // 'apiName' has to be at index 0 since 'node' record type could only be of 'recordQuery' or 'childRelationship'. They can not have the 'InlineFragmentNode' as its children.
9533
- const recordQueryApiName = objectInfoApiMap[ancestorPath]
9534
- ? objectInfoApiMap[ancestorPath][0]
9683
+ const recordQueryApiName = pathToObjectApiNamesMap[ancestorPath]
9684
+ ? pathToObjectApiNamesMap[ancestorPath][0]
9535
9685
  : recordQueryField.name.value;
9536
9686
  const nodeWithFragments = {
9537
9687
  ...node,
@@ -9568,7 +9718,7 @@ async function injectSyntheticFields(originalAST, objectInfoService, draftFuncti
9568
9718
  if (node.name.value === 'where') {
9569
9719
  const ancestorPath = findAncesterPath(ancestors);
9570
9720
  if (idState.paths.includes(ancestorPath)) {
9571
- const apiName = objectInfoApiMap[ancestorPath][0];
9721
+ const apiName = pathToObjectApiNamesMap[ancestorPath][0];
9572
9722
  const objectInfo = objectInfos[apiName];
9573
9723
  const swappedIdFilter = swapIdField(node.value, objectInfo, false, idState, draftFunctions);
9574
9724
  return {
@@ -9634,8 +9784,8 @@ function swapIdField(filterFields, objectInfo, swapped, idState, draftFunctions)
9634
9784
  };
9635
9785
  }
9636
9786
  }
9637
- function isMineScopeAvailable(apiNamePath, objectInfoApiMap, objectInfos) {
9638
- const apiName = objectInfoApiMap[apiNamePath];
9787
+ function isMineScopeAvailable(apiNamePath, pathToObjectApiNamesMap, objectInfos) {
9788
+ const apiName = pathToObjectApiNamesMap[apiNamePath];
9639
9789
  if (!apiName)
9640
9790
  return false;
9641
9791
  const objectInfo = objectInfos[apiName[0]];
@@ -9724,15 +9874,16 @@ function growObjectFieldTree(tree, parentNode, entryNode, totalNodes, startNodes
9724
9874
  }
9725
9875
  // example: 'Account'
9726
9876
  const childNode = objectFieldNode.name.value;
9877
+ const childNodepath = `${parentNode}#${childNode}`;
9727
9878
  if (!tree[parentNode].some((child) => child.name === childNode)) {
9728
9879
  tree[parentNode].push({
9729
9880
  relation: 'parent',
9730
9881
  name: childNode,
9731
9882
  });
9732
- totalNodes.add(childNode);
9883
+ totalNodes.add(childNodepath);
9733
9884
  }
9734
9885
  // recursively go to deeper level of filter.
9735
- growObjectFieldTree(tree, childNode, objectFieldNode.value, totalNodes, startNodes);
9886
+ growObjectFieldTree(tree, childNodepath, objectFieldNode.value, totalNodes, startNodes);
9736
9887
  }
9737
9888
  }
9738
9889
  }
@@ -9767,19 +9918,20 @@ function growFieldTree(tree, parentSectionPath, entryNode, parentNode, totalNode
9767
9918
  }
9768
9919
  if (!tree[parentSectionPath].some((field) => field.name === fieldName)) {
9769
9920
  tree[parentSectionPath].push({
9770
- relation: relationType === 'parentRelationship' ||
9771
- relationType === 'polymorphicParentRelationship'
9921
+ relation: relationType === PARENT_RELATIONSHIP ||
9922
+ relationType === POLYMORPHIC_PARENT_RELATIONSHIP
9772
9923
  ? 'parent'
9773
9924
  : 'child',
9774
9925
  name: fieldName,
9775
9926
  });
9776
- totalNodes.add(fieldName);
9927
+ totalNodes.add(`${parentSectionPath}#${fieldName}`);
9777
9928
  }
9778
9929
  if (entryNode.selectionSet && entryNode.selectionSet.selections) {
9779
9930
  const childNodes = entryNode.selectionSet.selections.filter(isFieldOrInlineFragmentNode);
9780
9931
  // recursively build the traversal tree
9781
9932
  for (const child of childNodes) {
9782
- growFieldTree(tree, fieldName, child, entryNode, totalNodes, startNodes);
9933
+ const path = `${parentSectionPath}#${fieldName}`;
9934
+ growFieldTree(tree, path, child, entryNode, totalNodes, startNodes);
9783
9935
  }
9784
9936
  }
9785
9937
  }
@@ -9809,23 +9961,23 @@ function growFieldTree(tree, parentSectionPath, entryNode, parentNode, totalNode
9809
9961
  }
9810
9962
  if (!tree[parentSectionPath].some((field) => field.name === conditionName)) {
9811
9963
  tree[parentSectionPath].push({
9812
- relation: relationType === 'parentRelationship' ||
9813
- relationType === 'polymorphicParentRelationship'
9964
+ relation: relationType === PARENT_RELATIONSHIP ||
9965
+ relationType === POLYMORPHIC_PARENT_RELATIONSHIP
9814
9966
  ? 'parent'
9815
9967
  : 'child',
9816
9968
  name: conditionName,
9817
9969
  });
9818
- totalNodes.add(conditionName);
9970
+ const path = `${parentSectionPath}#${conditionName}`;
9971
+ totalNodes.add(path);
9819
9972
  }
9820
9973
  }
9821
9974
  }
9822
9975
  // dive deep immediately for 'InlineFragment'
9823
9976
  const childNodes = entryNode.selectionSet.selections.filter(isFieldOrInlineFragmentNode);
9977
+ const path = `${parentSectionPath}${entryNode.typeCondition ? '#' + entryNode.typeCondition.name.value : ''}`;
9824
9978
  // Navigates into InLineFragment
9825
9979
  for (const child of childNodes) {
9826
- growFieldTree(tree, entryNode.typeCondition
9827
- ? entryNode.typeCondition.name.value
9828
- : parentSectionPath, child, entryNode, totalNodes, startNodes);
9980
+ growFieldTree(tree, path, child, entryNode, totalNodes, startNodes);
9829
9981
  }
9830
9982
  }
9831
9983
  }
@@ -9837,7 +9989,7 @@ function growFieldTree(tree, parentSectionPath, entryNode, parentNode, totalNode
9837
9989
  * @param startNodes start nodes of the tree. It can be used to fetch ObjectInfo immediately
9838
9990
  * @param path
9839
9991
  */
9840
- async function resolveObjectInfos(objectInfotree, objectInfoApiMap, startNodes, objectInfoService) {
9992
+ async function resolveObjectInfos(objectInfotree, pathToObjectApiNamesMap, startNodes, objectInfoService) {
9841
9993
  let objectInfos;
9842
9994
  try {
9843
9995
  objectInfos = await objectInfoService.getObjectInfos(Array.from(startNodes));
@@ -9851,9 +10003,9 @@ async function resolveObjectInfos(objectInfotree, objectInfoApiMap, startNodes,
9851
10003
  throw new Error(`Unable to resolve ObjectInfo(s) for ${Array.from(startNodes)}`);
9852
10004
  }
9853
10005
  for (const startNode of startNodes) {
9854
- objectInfoApiMap[startNode] = [startNode];
10006
+ pathToObjectApiNamesMap[startNode] = [startNode];
9855
10007
  const children = objectInfotree[startNode];
9856
- const subObjectInfoMap = await fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfos, children, startNode, objectInfoService);
10008
+ const subObjectInfoMap = await fetchObjectInfos(objectInfotree, pathToObjectApiNamesMap, objectInfos, children, startNode, objectInfoService);
9857
10009
  objectInfos = { ...objectInfos, ...subObjectInfoMap };
9858
10010
  }
9859
10011
  return objectInfos;
@@ -9861,15 +10013,15 @@ async function resolveObjectInfos(objectInfotree, objectInfoApiMap, startNodes,
9861
10013
  // example 1: 'parentPath': 'ServiceAppointment', 'nodesAtSameLevel': ['Account']
9862
10014
  // example 2: 'parentPath': 'ServiceAppointment', 'nodesAtSameLevel': ['Owner'], this example has 2 apiName for the node 'Owner'
9863
10015
  // example 3: 'parentPath': 'ServiceAppointment_Owner', 'nodesAtSameLevel': ['User', 'Group']
9864
- async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap, nodesAtSameLevel, parentPath, objectInfoService) {
9865
- const objectInfoApiNames = objectInfoApiMap[parentPath];
10016
+ async function fetchObjectInfos(objectInfotree, pathToObjectApiNamesMap, objectInfoMap, nodesAtSameLevel, parentPath, objectInfoService) {
10017
+ const objectInfoApiNames = pathToObjectApiNamesMap[parentPath];
9866
10018
  if (!objectInfoApiNames) {
9867
10019
  // eslint-disable-next-line
9868
10020
  throw new Error(`Object Info does not exist for ${parentPath}`);
9869
10021
  }
9870
10022
  const validObjectInfoNodes = [];
9871
10023
  let updatedObjectInfoMap = {};
9872
- // InlineFragment and polymorphic field support fits into this scenario ObjectInfoApiMap Entry: 'ServiceAppointment_Owner' -> ['User', 'Group']; ServiceAppointment_Owner_User' -> ['User']
10024
+ // InlineFragment and polymorphic field support fits into this scenario pathToObjectApiNamesMap Entry: 'ServiceAppointment#Owner' -> ['User', 'Group']; ServiceAppointment#Owner#User' -> ['User']
9873
10025
  if (objectInfoApiNames.length > 0 &&
9874
10026
  nodesAtSameLevel.length > 0 &&
9875
10027
  objectInfoApiNames.includes(nodesAtSameLevel[0].name)) {
@@ -9881,8 +10033,8 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9881
10033
  // eslint-disable-next-line
9882
10034
  throw new Error(`Condition ${field.name} does not exists for ${parentPath}`);
9883
10035
  }
9884
- const path = `${parentPath}_${field.name}`;
9885
- objectInfoApiMap[path] = [field.name];
10036
+ const path = `${parentPath}#${field.name}`;
10037
+ pathToObjectApiNamesMap[path] = [field.name];
9886
10038
  }
9887
10039
  validObjectInfoNodes.push(...nodesAtSameLevel);
9888
10040
  updatedObjectInfoMap = { ...objectInfoMap };
@@ -9897,7 +10049,7 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9897
10049
  let apiNames = [];
9898
10050
  for (const nodeInfo of nodesAtSameLevel) {
9899
10051
  const field = nodeInfo.name;
9900
- const path = `${parentPath}_${field}`;
10052
+ const path = `${parentPath}#${field}`;
9901
10053
  // Handle 'parentRelationship'
9902
10054
  if (nodeInfo.relation === 'parent') {
9903
10055
  const relationshipId = referenceIdFieldForRelationship(field);
@@ -9915,21 +10067,21 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9915
10067
  }
9916
10068
  }
9917
10069
  // This is a polymorphic field
9918
- if (fieldDefinition.referenceToInfos.length > 1 && objectInfotree[field]) {
10070
+ if (fieldDefinition.referenceToInfos.length > 1 && objectInfotree[path]) {
9919
10071
  // Fields needs to expand and heterogenous entity ObjectInfo needs to be fetched
9920
- const referencedNodeInfos = objectInfotree[field];
10072
+ const referencedNodeInfos = objectInfotree[path];
9921
10073
  const requestedApiNames = referencedNodeInfos.map((referenceNodeInfo) => referenceNodeInfo.name);
9922
10074
  // Fetches requested ObjectInfo only. Some entity's relation field could define more than 6 references. Only references show up in query need to be handled.
9923
- if (requestedApiNames.length > 0 && objectInfotree[field]) {
10075
+ if (requestedApiNames.length > 0 && objectInfotree[path]) {
9924
10076
  fieldDefinition.referenceToInfos
9925
10077
  .filter((referenceToInfo) => requestedApiNames.includes(referenceToInfo.apiName))
9926
10078
  .forEach((ref) => {
9927
- if (!objectInfoApiMap[path]) {
9928
- objectInfoApiMap[path] = [];
10079
+ if (!pathToObjectApiNamesMap[path]) {
10080
+ pathToObjectApiNamesMap[path] = [];
9929
10081
  }
9930
10082
  // 'ServiceAppointment_Owner' ->['User', 'Group']
9931
- if (!objectInfoApiMap[path].includes(ref.apiName)) {
9932
- objectInfoApiMap[path].push(ref.apiName);
10083
+ if (!pathToObjectApiNamesMap[path].includes(ref.apiName)) {
10084
+ pathToObjectApiNamesMap[path].push(ref.apiName);
9933
10085
  }
9934
10086
  if (!apiNames.includes(ref.apiName)) {
9935
10087
  apiNames.push(ref.apiName);
@@ -9939,11 +10091,11 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9939
10091
  }
9940
10092
  else if (fieldDefinition.referenceToInfos.length === 1) {
9941
10093
  const ref = fieldDefinition.referenceToInfos[0];
9942
- if (!objectInfoApiMap[path]) {
9943
- objectInfoApiMap[path] = [];
10094
+ if (!pathToObjectApiNamesMap[path]) {
10095
+ pathToObjectApiNamesMap[path] = [];
9944
10096
  }
9945
- if (!objectInfoApiMap[path].includes(ref.apiName)) {
9946
- objectInfoApiMap[path].push(ref.apiName);
10097
+ if (!pathToObjectApiNamesMap[path].includes(ref.apiName)) {
10098
+ pathToObjectApiNamesMap[path].push(ref.apiName);
9947
10099
  }
9948
10100
  if (!apiNames.includes(ref.apiName)) {
9949
10101
  apiNames.push(ref.apiName);
@@ -9954,11 +10106,11 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9954
10106
  // handles 'childRelationship'
9955
10107
  const childRelationship = parentObjectInfo.childRelationships.find((childRelationship) => childRelationship.relationshipName === field);
9956
10108
  if (childRelationship) {
9957
- if (!objectInfoApiMap[path]) {
9958
- objectInfoApiMap[path] = [];
10109
+ if (!pathToObjectApiNamesMap[path]) {
10110
+ pathToObjectApiNamesMap[path] = [];
9959
10111
  }
9960
- if (!objectInfoApiMap[path].includes(childRelationship.childObjectApiName)) {
9961
- objectInfoApiMap[path].push(childRelationship.childObjectApiName);
10112
+ if (!pathToObjectApiNamesMap[path].includes(childRelationship.childObjectApiName)) {
10113
+ pathToObjectApiNamesMap[path].push(childRelationship.childObjectApiName);
9962
10114
  }
9963
10115
  if (!apiNames.includes(childRelationship.childObjectApiName)) {
9964
10116
  apiNames.push(childRelationship.childObjectApiName);
@@ -9980,10 +10132,10 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9980
10132
  }
9981
10133
  for (const nodeInfo of validObjectInfoNodes) {
9982
10134
  const field = nodeInfo.name;
9983
- const subLevelFields = objectInfotree[field];
9984
- const path = `${parentPath}_${field}`;
10135
+ const path = `${parentPath}#${field}`;
10136
+ const subLevelFields = objectInfotree[path];
9985
10137
  if (subLevelFields && subLevelFields.length > 0) {
9986
- const subObjectInfos = await fetchObjectInfos(objectInfotree, objectInfoApiMap, updatedObjectInfoMap, subLevelFields, path, objectInfoService);
10138
+ const subObjectInfos = await fetchObjectInfos(objectInfotree, pathToObjectApiNamesMap, updatedObjectInfoMap, subLevelFields, path, objectInfoService);
9987
10139
  updatedObjectInfoMap = { ...updatedObjectInfoMap, ...subObjectInfos };
9988
10140
  }
9989
10141
  }
@@ -9998,27 +10150,29 @@ async function fetchObjectInfos(objectInfotree, objectInfoApiMap, objectInfoMap,
9998
10150
  * 'path' and 'queryNode' is 1 level above the 'filterNode'
9999
10151
  * @param filterNode filter node which needs to be injected. For example, 'State' ObjectFieldNode within filter 'where: { State: { eq: "Nova Scotia" }}'
10000
10152
  * @param idState ID state will be updated to determine if the ID fields in AST need to be swapped. The swapping happens later.
10001
- * @param path path to the current filterNode's parent. For example, path could be 'ServiceApointment' when filterNode is 'State'. If the path does not exist in 'objectInfoApiMap', parent node is not an field of relationship or recordQuery
10153
+ * @param parentPath path to the current filterNode's parent. For example, path could be 'ServiceApointment' when filterNode is 'State'. If the path does not exist in 'pathToObjectApiNamesMap', parent node is not an field of relationship or recordQuery
10154
+ * @param isParentPolymorphic true if parent points to a polymorphic field.
10002
10155
  * @param queryNode referece FieldNode which provides the information if 'filterNode' exist in it nor not.
10003
10156
  * @param objectInfos ObjectInfo map used in injection. If ObjectInfo misses or field does not exist in ObjectInfo, the error will be thrown
10004
- * @param objectInfoApiMap map used to locate the ObjectInfo. The key is path to a field, value is the ObjectInfo's apiName array. In the case of polymorphic fields, the apiName array have 2 or more elements. For example, 'ServiceAppointment' -> ['ServiceAppointment']; 'ServiceAppointment_Account' -> ['Account'], 'ServiceAppointment_Owner' -> ['User', 'Group'].
10157
+ * @param pathToObjectApiNamesMap map used to locate the ObjectInfo. The key is path to a field, value is the ObjectInfo's apiName array. In the case of polymorphic fields, the apiName array have 2 or more elements. For example, 'ServiceAppointment' -> ['ServiceAppointment']; 'ServiceAppointment_Account' -> ['Account'], 'ServiceAppointment_Owner' -> ['User', 'Group'].
10005
10158
  * @param draftFunctions functions for working with record ids that may be draft-created ids
10006
10159
  * @returns an array of nodes with injected fields
10007
10160
  */
10008
- function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap, draftFunctions, queryNode) {
10161
+ function injectFilter(filterNode, idState, parentPath, isParentPolymorphic, objectInfos, pathToObjectApiNamesMap, draftFunctions, queryNode) {
10009
10162
  const injectedSelections = [];
10163
+ let isPolymorphicField = false;
10010
10164
  switch (filterNode.kind) {
10011
10165
  case Kind.ARGUMENT:
10012
10166
  if (filterNode.value.kind !== 'ObjectValue')
10013
10167
  return [];
10014
10168
  filterNode.value.fields.forEach((objectFieldNode) => {
10015
- let subResults = injectFilter(objectFieldNode, idState, path, objectInfos, objectInfoApiMap, draftFunctions, queryNode);
10169
+ let subResults = injectFilter(objectFieldNode, idState, parentPath, isParentPolymorphic, objectInfos, pathToObjectApiNamesMap, draftFunctions, queryNode);
10016
10170
  for (const subResult of subResults) {
10017
10171
  mergeOrAddToGroup(injectedSelections, subResult);
10018
10172
  }
10019
10173
  // multiple Ids might need to be swapped. remember their paths for faster write.
10020
10174
  if (idState.swapNeeded) {
10021
- idState.paths.push(path);
10175
+ idState.paths.push(parentPath);
10022
10176
  }
10023
10177
  });
10024
10178
  return injectedSelections;
@@ -10027,7 +10181,7 @@ function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap,
10027
10181
  case Kind.LIST: {
10028
10182
  filterNode.value.values.filter(isObjectValueNode).forEach((objectValueNode) => {
10029
10183
  objectValueNode.fields.forEach((objectFieldNode) => {
10030
- const subResults = injectFilter(objectFieldNode, idState, path, objectInfos, objectInfoApiMap, draftFunctions, queryNode);
10184
+ const subResults = injectFilter(objectFieldNode, idState, parentPath, isParentPolymorphic, objectInfos, pathToObjectApiNamesMap, draftFunctions, queryNode);
10031
10185
  for (const subResult of subResults) {
10032
10186
  mergeOrAddToGroup(injectedSelections, subResult);
10033
10187
  }
@@ -10038,7 +10192,7 @@ function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap,
10038
10192
  case Kind.OBJECT: {
10039
10193
  if (filterNode.name.value === 'not') {
10040
10194
  filterNode.value.fields.forEach((objectFieldNode) => {
10041
- const subResults = injectFilter(objectFieldNode, idState, path, objectInfos, objectInfoApiMap, draftFunctions, queryNode);
10195
+ const subResults = injectFilter(objectFieldNode, idState, parentPath, isParentPolymorphic, objectInfos, pathToObjectApiNamesMap, draftFunctions, queryNode);
10042
10196
  for (const subResult of subResults) {
10043
10197
  mergeOrAddToGroup(injectedSelections, subResult);
10044
10198
  }
@@ -10048,15 +10202,15 @@ function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap,
10048
10202
  let apiNames = [];
10049
10203
  let isScalarField = false;
10050
10204
  //It is possible that this is a polymorphic field
10051
- apiNames = objectInfoApiMap[path];
10052
- // example: path: 'ServiceAppointment_LastModifiedDate'; filterNode: '{eq: {literal: LAST_WEEK}}'. queryNode: 'LastModifedDate { value}' FilterNode's parent has been verifed as a valid node
10205
+ apiNames = pathToObjectApiNamesMap[parentPath];
10206
+ // example: path: 'ServiceAppointment#LastModifiedDate'; filterNode: '{eq: {literal: LAST_WEEK}}'. queryNode: 'LastModifedDate { value}' FilterNode's parent has been verifed as a valid node
10053
10207
  if (apiNames === undefined) {
10054
10208
  isScalarField = true;
10055
10209
  }
10056
10210
  else {
10057
10211
  if (apiNames.some((apiName) => objectInfos[apiName] === undefined)) {
10058
10212
  // eslint-disable-next-line
10059
- throw new Error(`ObjectInfo is missing for ${path}`);
10213
+ throw new Error(`ObjectInfo is missing for ${parentPath}`);
10060
10214
  }
10061
10215
  }
10062
10216
  if (isScalarField) {
@@ -10078,29 +10232,19 @@ function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap,
10078
10232
  }
10079
10233
  });
10080
10234
  let isSpanning = false;
10235
+ // if true, current node is a polymorphic concrete type node. For example, field node `User` under `Owner`
10081
10236
  let isInlineFragment = false;
10082
- let isPolymorphicField = false;
10083
10237
  let isTypeNameExisting = false;
10084
10238
  let curPath;
10085
10239
  let fieldName = filterNode.name.value;
10086
- curPath = `${path}_${fieldName}`;
10087
- if (objectInfoApiMap[curPath] && objectInfoApiMap[curPath].length > 0) {
10240
+ curPath = `${parentPath}#${fieldName}`;
10241
+ if (pathToObjectApiNamesMap[curPath] &&
10242
+ pathToObjectApiNamesMap[curPath].length > 0) {
10088
10243
  isSpanning = true;
10089
- if (objectInfoApiMap[curPath].length === 1) {
10090
- if (objectInfoApiMap[path] &&
10091
- objectInfoApiMap[path].length >= 1 &&
10092
- objectInfoApiMap[path].includes(objectInfoApiMap[curPath][0])) {
10093
- isInlineFragment = true;
10094
- }
10095
- }
10096
- // Checks if the current filter node is a polymorphic field. 'ServiceAppointment_Owner' --> ['User']; 'ServiceAppointment_Owner_User' --> ['User']
10097
- const childApiName = objectInfoApiMap[curPath][0];
10098
- const trialApiNames = objectInfoApiMap[`${curPath}_${childApiName}`];
10099
- if (trialApiNames !== undefined &&
10100
- trialApiNames.length === 1 &&
10101
- trialApiNames[0] === childApiName) {
10102
- isPolymorphicField = true;
10103
- }
10244
+ isInlineFragment =
10245
+ isParentPolymorphic &&
10246
+ pathToObjectApiNamesMap[curPath].length === 1;
10247
+ isPolymorphicField = isPolymorphicFieldPath(curPath, pathToObjectApiNamesMap, objectInfos);
10104
10248
  }
10105
10249
  // When filter node is at InLineFragment Level(a concrete entity of polymorphic field), query node is one level up. For example, ObjectFieldNode is ...{User:{...}}, queryNode is Owner:[User]
10106
10250
  if (isInlineFragment) {
@@ -10144,23 +10288,25 @@ function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap,
10144
10288
  throw new Error(`Field ${fieldName} does not exist in ${apiNames[0]}`);
10145
10289
  }
10146
10290
  }
10147
- const objectInfoName = objectInfoApiMap[curPath] !== undefined
10148
- ? objectInfoApiMap[curPath][0]
10149
- : objectInfoApiMap[path][0];
10291
+ const objectInfoName = pathToObjectApiNamesMap[curPath] !== undefined
10292
+ ? pathToObjectApiNamesMap[curPath][0]
10293
+ : pathToObjectApiNamesMap[parentPath][0];
10150
10294
  const isIdField = isFieldAnIdField(filterNode.name.value, objectInfos[objectInfoName]);
10151
10295
  if (!isIdField) {
10152
10296
  let subSelectionNodes = [];
10153
10297
  let subFieldsHasId = false;
10154
- filterNode.value.fields.forEach((subFieldNode) => {
10155
- // Check if the filter field has the 'Id'
10156
- if (isFieldAnIdField(subFieldNode.name.value, objectInfos[objectInfoName])) {
10157
- subFieldsHasId = true;
10158
- updateIDInfo(subFieldNode, idState, draftFunctions);
10159
- }
10160
- // try injecting the fields within predicate no matter it has relation or not.
10161
- let subResults = injectFilter(subFieldNode, idState, curPath, objectInfos, objectInfoApiMap, draftFunctions, existingFields ? existingFields[0] : undefined);
10162
- subSelectionNodes = subSelectionNodes.concat(subResults);
10163
- });
10298
+ if (isSpanning) {
10299
+ filterNode.value.fields.forEach((subFieldNode) => {
10300
+ // Check if the filter field has the 'Id'
10301
+ if (isFieldAnIdField(subFieldNode.name.value, objectInfos[objectInfoName])) {
10302
+ subFieldsHasId = true;
10303
+ updateIDInfo(subFieldNode, idState, draftFunctions);
10304
+ }
10305
+ // try injecting the fields within predicate no matter it has relation or not.
10306
+ let subResults = injectFilter(subFieldNode, idState, curPath, isPolymorphicField, objectInfos, pathToObjectApiNamesMap, draftFunctions, existingFields ? existingFields[0] : undefined);
10307
+ subSelectionNodes = subSelectionNodes.concat(subResults);
10308
+ });
10309
+ }
10164
10310
  if (!subFieldsHasId) {
10165
10311
  // Check if the query field has the 'Id'
10166
10312
  const existingIdsInQuery = existingFields &&
@@ -10189,6 +10335,7 @@ function injectFilter(filterNode, idState, path, objectInfos, objectInfoApiMap,
10189
10335
  }
10190
10336
  //Inject Conditions: 1. Same field does not exist 2. Same fields has different children. 3. Filter spanning field does not have Id. 4. InLineFragment does not have the '__typename' field
10191
10337
  if (!existingFields ||
10338
+ existingFields.length === 0 ||
10192
10339
  subSelectionNodes.length > 0 ||
10193
10340
  (isSpanning && !subFieldsHasId) ||
10194
10341
  (isInlineFragment && !isTypeNameExisting)) {
@@ -10319,6 +10466,44 @@ function mergeOrAddToGroup(group, element) {
10319
10466
  }
10320
10467
  group.push(element);
10321
10468
  }
10469
+ // checks if the path points to a polymorphic field. For example, for the below `pathToObjectApiNamesMap`
10470
+ // {
10471
+ // 'ServiceAppointment' -> ['ServiceAppointment']
10472
+ // 'ServiceAppointment#Owner' --> ['User'],
10473
+ // 'ServiceAppointment#Owner#User' --> ['User']
10474
+ // }
10475
+ // path `ServiceAppointment#Owner` points to a polymorphic field, but path `ServiceAppointment#Owner#User` does not.
10476
+ function isPolymorphicFieldPath(path, pathToObjectApiNamesMap, objectInfos) {
10477
+ const lastSegmentIndex = path.lastIndexOf('#');
10478
+ if (lastSegmentIndex < 0) {
10479
+ return false;
10480
+ }
10481
+ const lastSegment = path.slice(lastSegmentIndex + 1);
10482
+ const parentApiPath = path.slice(0, lastSegmentIndex);
10483
+ if (pathToObjectApiNamesMap[parentApiPath] === undefined) {
10484
+ return false;
10485
+ }
10486
+ const parentObjectApiNames = pathToObjectApiNamesMap[parentApiPath];
10487
+ // If the last segment is a Polymorphic field, its immediate parent is a concrete object entity, which has 1 objectApiName mapped to the parent path in `pathToObjectApiNamesMap`.
10488
+ // For example, we like to check if `ServiceAppointment#Owner` path is polymorphic. The last segment is `Owner` and its parent `ServiceAppointment` has one element (which is also `ServiceAppointment`) array as its value.
10489
+ // Below are the entries in `pathToObjectApiNamesMap`
10490
+ // {
10491
+ // `ServiceAppointmen`t: [`ServiceAppointment`],
10492
+ // `ServiceAppointment#Owner`: [`User`, `Group`],
10493
+ // `ServiceAppointment#Owner#User`: [`User`],
10494
+ // `ServiceAppointment#Owner#Group`: [`Group`],
10495
+ // }
10496
+ if (parentObjectApiNames.length !== 1) {
10497
+ return false;
10498
+ }
10499
+ const parentObjectInfo = objectInfos[parentObjectApiNames[0]];
10500
+ const relationshipField = referenceIdFieldForRelationship(lastSegment);
10501
+ let fieldDefinition = parentObjectInfo.fields[relationshipField];
10502
+ if (fieldDefinition === undefined) {
10503
+ return false;
10504
+ }
10505
+ return fieldDefinition.polymorphicForeignKey;
10506
+ }
10322
10507
  function isFieldAnIdField(fieldName, objectInfo) {
10323
10508
  if (fieldName === 'Id')
10324
10509
  return true;
@@ -10371,10 +10556,10 @@ function updateIDInfo(fieldNode, idState, draftFunctions) {
10371
10556
  * @param parentNode parent node of param 1
10372
10557
  * @param ancestors ancester of param 1
10373
10558
  * @param objectInfos ObjectInfo map used in injection. If ObjectInfo misses or field does not exist in ObjectInfo, the error will be thrown
10374
- * @param objectInfoApiMap map used to locate the ObjectInfo. The key is path to a field, value is the ObjectInfo's apiName array. In the case of polymorphic fields, the apiName array have 2 or more elements. For example, 'ServiceAppointment' -> ['ServiceAppointment']; 'ServiceAppointment_Account' -> ['Account'], 'ServiceAppointment_Owner' -> ['User', 'Group'].
10559
+ * @param pathToObjectApiNamesMap map used to locate the ObjectInfo. The key is path to a field, value is the ObjectInfo's apiName array. In the case of polymorphic fields, the apiName array have 2 or more elements. For example, 'ServiceAppointment' -> ['ServiceAppointment']; 'ServiceAppointment#Account' -> ['Account'], 'ServiceAppointment#Owner' -> ['User', 'Group'].
10375
10560
  * @return injected SelectionNodes used to construct the InlineFragment.
10376
10561
  */
10377
- function injectFields(selections, parentNode, ancestors, objectInfos, objectInfoApiMap) {
10562
+ function injectFields(selections, parentNode, parentPath, ancestors, objectInfos, pathToObjectApiNamesMap) {
10378
10563
  /**
10379
10564
  * 1 parentship can return 2 FieldNode which need to be flattened
10380
10565
  * Concact: { ** Contact { ** ContactId {
@@ -10392,6 +10577,10 @@ function injectFields(selections, parentNode, ancestors, objectInfos, objectInfo
10392
10577
  if (!selection.selectionSet) {
10393
10578
  return selection;
10394
10579
  }
10580
+ const segment = isFieldNode(selection)
10581
+ ? selection.name.value
10582
+ : selection.typeCondition.name.value;
10583
+ const curPath = `${parentPath}#${segment}`;
10395
10584
  const spanningSubSelections = [];
10396
10585
  for (const subSelection of selection.selectionSet.selections) {
10397
10586
  if (isFieldSpanning(subSelection, selection)) {
@@ -10399,7 +10588,7 @@ function injectFields(selections, parentNode, ancestors, objectInfos, objectInfo
10399
10588
  }
10400
10589
  }
10401
10590
  // Handles multiple level field injection like 'ServiceAppointment' --> 'Account' --> 'Owner'
10402
- const subInjectedSelections = injectFields(spanningSubSelections, selection, ancestors, objectInfos, objectInfoApiMap);
10591
+ const subInjectedSelections = injectFields(spanningSubSelections, selection, curPath, ancestors, objectInfos, pathToObjectApiNamesMap);
10403
10592
  if (!selection.selectionSet) {
10404
10593
  return selection;
10405
10594
  }
@@ -10442,7 +10631,7 @@ function injectFields(selections, parentNode, ancestors, objectInfos, objectInfo
10442
10631
  }
10443
10632
  }
10444
10633
  // For polymorphic fields, the Id field is excluded.
10445
- const excludeId = selection.selectionSet.selections.every(isInlineFragmentNode);
10634
+ const excludeId = isPolymorphicFieldPath(curPath, pathToObjectApiNamesMap, objectInfos);
10446
10635
  const idSelection = [];
10447
10636
  if (!excludeId && !hasIdAlready) {
10448
10637
  idSelection.push({
@@ -10453,8 +10642,8 @@ function injectFields(selections, parentNode, ancestors, objectInfos, objectInfo
10453
10642
  },
10454
10643
  });
10455
10644
  }
10456
- // Inject '__typename' for polymorphic fields. '__typename' field acts as a reference to concrete type of a polymorphic field and is used to match JSON response with AST node. For more detail,
10457
- // please reference 'removeSyntheticFields'.
10645
+ // Inject '__typename' for InlineFragment. '__typename' field acts as a reference to concrete type of a polymorphic field or a standard field in the returned GQL response, which equals to
10646
+ // `typedCondition` of the InlineFragment in the query AST. It is used to match JSON response with AST node. For more detail, please reference 'removeSyntheticFields'.
10458
10647
  if (isInlineFragmentNode(selection) &&
10459
10648
  !selection.selectionSet.selections.find((selection) => isFieldNode(selection) && selection.name.value === '__typename')) {
10460
10649
  idSelection.push({
@@ -10488,7 +10677,7 @@ function injectFields(selections, parentNode, ancestors, objectInfos, objectInfo
10488
10677
  if (isFieldNode(parentNode) && parentNode.selectionSet && parentNode.name.value === 'node') {
10489
10678
  if (parentNode.selectionSet.selections
10490
10679
  .filter(isFieldOrInlineFragmentNode)
10491
- .some((selectionNode) => isRelationship(selectionNode, 'childRelationship'))) {
10680
+ .some((selectionNode) => isRelationship(selectionNode, CHILD_RELATIONSHIP))) {
10492
10681
  if (!parentNode.selectionSet.selections
10493
10682
  .filter(isFieldNode)
10494
10683
  .some((sibling) => sibling.name.value === 'Id')) {
@@ -10507,15 +10696,15 @@ function injectFields(selections, parentNode, ancestors, objectInfos, objectInfo
10507
10696
  if (parentInfo.parentIndex >= 0) {
10508
10697
  // example node { TimeSheetEntries { edges { node { Id }}}}
10509
10698
  const parent = parentInfo.node;
10510
- if (isRelationship(parent, 'childRelationship')) {
10699
+ if (isRelationship(parent, CHILD_RELATIONSHIP)) {
10511
10700
  const unVisitedAncestors = ancestors.slice(0, parentInfo.parentIndex);
10512
10701
  // path : "TimeSheet"
10513
10702
  const grandParentPath = findAncesterPath(unVisitedAncestors);
10514
- if (objectInfoApiMap &&
10515
- objectInfoApiMap[grandParentPath] &&
10703
+ if (pathToObjectApiNamesMap &&
10704
+ pathToObjectApiNamesMap[grandParentPath] &&
10516
10705
  objectInfos &&
10517
- objectInfos[objectInfoApiMap[grandParentPath][0]]) {
10518
- const grandParentObjectInfo = objectInfos[objectInfoApiMap[grandParentPath][0]];
10706
+ objectInfos[pathToObjectApiNamesMap[grandParentPath][0]]) {
10707
+ const grandParentObjectInfo = objectInfos[pathToObjectApiNamesMap[grandParentPath][0]];
10519
10708
  // exmaple "TimeSheetEntries"
10520
10709
  const parentFieldName = parent.name.value;
10521
10710
  const targetRelationship = grandParentObjectInfo.childRelationships.find((childRelationship) => {
@@ -10636,7 +10825,7 @@ const assignedToMeFragmentSelections = [
10636
10825
  },
10637
10826
  value: {
10638
10827
  kind: 'StringValue',
10639
- value: 'childRelationship',
10828
+ value: CHILD_RELATIONSHIP,
10640
10829
  block: false,
10641
10830
  },
10642
10831
  },
@@ -10728,7 +10917,7 @@ const assignedToMeFragmentSelections = [
10728
10917
  },
10729
10918
  value: {
10730
10919
  kind: 'StringValue',
10731
- value: 'parentRelationship',
10920
+ value: PARENT_RELATIONSHIP,
10732
10921
  block: false,
10733
10922
  },
10734
10923
  },
@@ -10865,7 +11054,9 @@ function handleNonArrayJsonProperty(selection, fieldName, jsonInput, jsonOutput)
10865
11054
  jsonOutput[fieldName] = null;
10866
11055
  return;
10867
11056
  }
10868
- jsonOutput[fieldName] = {};
11057
+ if (jsonOutput[fieldName] === undefined) {
11058
+ jsonOutput[fieldName] = {};
11059
+ }
10869
11060
  createUserJsonOutput(selection, jsonInput[fieldName], jsonOutput[fieldName]);
10870
11061
  }
10871
11062
  else {
@@ -11597,34 +11788,42 @@ function applyReferenceLinksToDraft(record, draftMetadata) {
11597
11788
  }
11598
11789
  const { dataType, relationshipName, referenceToInfos } = fieldInfo;
11599
11790
  const draftFieldValue = record.fields[draftField].value;
11600
- if (dataType === 'Reference' && relationshipName !== null && draftFieldValue !== null) {
11601
- if (typeof draftFieldValue !== 'string') {
11602
- throw Error('reference field value is not a string');
11791
+ if (dataType === 'Reference' && relationshipName !== null) {
11792
+ if (draftFieldValue === null) {
11793
+ recordFields[relationshipName] = {
11794
+ displayValue: null,
11795
+ value: null,
11796
+ };
11603
11797
  }
11604
- const key = getRecordKeyForId(luvio, draftFieldValue);
11605
- const referencedRecord = referencedRecords.get(key);
11606
- recordFields[relationshipName] = {
11607
- displayValue: null,
11608
- value: createLink(key),
11609
- };
11610
- // for custom objects, we select the 'Name' field
11611
- // otherwise we check the object info for name fields.
11612
- //if there are multiple we select 'Name' if it exists, otherwise the first one
11613
- if (referencedRecord !== undefined && referenceToInfos.length > 0) {
11614
- let nameField;
11615
- const referenceToInfo = referenceToInfos[0];
11616
- const nameFields = referenceToInfo.nameFields;
11617
- if (nameFields.length !== 0) {
11618
- nameField = nameFields.find((x) => x === 'Name');
11619
- if (nameField === undefined) {
11620
- nameField = nameFields[0];
11621
- }
11798
+ else {
11799
+ if (typeof draftFieldValue !== 'string') {
11800
+ throw Error('reference field value is not a string');
11622
11801
  }
11623
- if (nameField !== undefined) {
11624
- const nameFieldRef = referencedRecord.fields[nameField];
11625
- if (nameFieldRef) {
11626
- recordFields[relationshipName].displayValue =
11627
- (_a = nameFieldRef.displayValue) !== null && _a !== void 0 ? _a : nameFieldRef.value;
11802
+ const key = getRecordKeyForId(luvio, draftFieldValue);
11803
+ const referencedRecord = referencedRecords.get(key);
11804
+ recordFields[relationshipName] = {
11805
+ displayValue: null,
11806
+ value: createLink(key),
11807
+ };
11808
+ // for custom objects, we select the 'Name' field
11809
+ // otherwise we check the object info for name fields.
11810
+ //if there are multiple we select 'Name' if it exists, otherwise the first one
11811
+ if (referencedRecord !== undefined && referenceToInfos.length > 0) {
11812
+ let nameField;
11813
+ const referenceToInfo = referenceToInfos[0];
11814
+ const nameFields = referenceToInfo.nameFields;
11815
+ if (nameFields.length !== 0) {
11816
+ nameField = nameFields.find((x) => x === 'Name');
11817
+ if (nameField === undefined) {
11818
+ nameField = nameFields[0];
11819
+ }
11820
+ }
11821
+ if (nameField !== undefined) {
11822
+ const nameFieldRef = referencedRecord.fields[nameField];
11823
+ if (nameFieldRef) {
11824
+ recordFields[relationshipName].displayValue =
11825
+ (_a = nameFieldRef.displayValue) !== null && _a !== void 0 ? _a : nameFieldRef.value;
11826
+ }
11628
11827
  }
11629
11828
  }
11630
11829
  }
@@ -11917,17 +12116,8 @@ class UiApiActionHandler extends AbstractResourceRequestActionHandler {
11917
12116
  };
11918
12117
  for (const fieldName of keys$3(recordWithSpanningRefLinks.fields)) {
11919
12118
  const fieldKey = buildRecordFieldStoreKey(key, fieldName);
11920
- if (this.collectedFields[fieldKey] !== undefined) {
11921
- const fieldData = recordWithSpanningRefLinks.fields[fieldName];
11922
- normalizedRecord.fields[fieldName] = { __ref: fieldKey };
11923
- publishData(fieldKey, fieldData);
11924
- }
11925
- else if (recordWithSpanningRefLinks.fields[fieldName] &&
11926
- recordWithSpanningRefLinks.fields[fieldName].value &&
11927
- recordWithSpanningRefLinks.fields[fieldName].value.__ref !== undefined) {
11928
- normalizedRecord.fields[fieldName] = { __ref: fieldKey };
11929
- publishData(fieldKey, recordWithSpanningRefLinks.fields[fieldName]);
11930
- }
12119
+ normalizedRecord.fields[fieldName] = { __ref: fieldKey };
12120
+ publishData(fieldKey, recordWithSpanningRefLinks.fields[fieldName]);
11931
12121
  }
11932
12122
  // publish the normalized record
11933
12123
  publishData(key, normalizedRecord);
@@ -12826,6 +13016,9 @@ function draftAwareGraphQLAdapterFactory(userId, objectInfoService, store, luvio
12826
13016
  if (!rebuildResult.errors) {
12827
13017
  rebuildResult = removeSyntheticFields(rebuildResult, config.query);
12828
13018
  }
13019
+ if (objectsDeepEqual(rebuildResult, originalSnapshot.data)) {
13020
+ return originalSnapshot;
13021
+ }
12829
13022
  // 'originalSnapshot' is the local eval snapshot subscribed. It is always in 'Fulfilled' state. This behavior would change once W-1273462(rebuild non-evaluated snapshot when the graphql local eval rebuild is triggered) is resolved.
12830
13023
  return {
12831
13024
  ...originalSnapshot,
@@ -12844,7 +13037,7 @@ function draftAwareGraphQLAdapterFactory(userId, objectInfoService, store, luvio
12844
13037
  // Fulfilled snapshot (this only happens in this code path if
12845
13038
  // the error is network error or 504), otherwise we spread over
12846
13039
  // the non-eval'ed snapshot (which will be either Fulfilled or Stale)
12847
- return nonEvaluatedSnapshot.state === 'Error'
13040
+ const resultSnapshot = nonEvaluatedSnapshot.state === 'Error'
12848
13041
  ? createLocalEvalSnapshot(gqlResult, seenRecords, recordId, rebuildWithLocalEval)
12849
13042
  : {
12850
13043
  ...nonEvaluatedSnapshot,
@@ -12853,6 +13046,22 @@ function draftAwareGraphQLAdapterFactory(userId, objectInfoService, store, luvio
12853
13046
  seenRecords,
12854
13047
  rebuildWithLocalEval,
12855
13048
  };
13049
+ const { refresh, state } = resultSnapshot;
13050
+ if (state !== 'Error' && refresh) {
13051
+ // after refreshing a graphql snapshot, we want to force a rebuild regardless
13052
+ // of if the call failed or not or if the data changed or not because we want
13053
+ // to make sure any potential new drafts are picked up
13054
+ resultSnapshot.refresh = {
13055
+ ...refresh,
13056
+ resolve: (config) => {
13057
+ return refresh.resolve(config).finally(() => {
13058
+ luvio.storePublish(resultSnapshot.recordId, undefined);
13059
+ luvio.storeBroadcast();
13060
+ });
13061
+ },
13062
+ };
13063
+ }
13064
+ return resultSnapshot;
12856
13065
  };
12857
13066
  }
12858
13067
 
@@ -13907,7 +14116,10 @@ function isErrorResponse(response) {
13907
14116
  * @returns the merged record
13908
14117
  */
13909
14118
  function mergeAggregateUiResponse(response, mergeFunc) {
13910
- const { body } = response;
14119
+ if (response.ok === false) {
14120
+ return response;
14121
+ }
14122
+ const body = response.body;
13911
14123
  try {
13912
14124
  if (body === null ||
13913
14125
  body === undefined ||
@@ -15870,6 +16082,9 @@ class PrimingSession extends EventEmitter {
15870
16082
  this.ldsRecordRefresher = config.ldsRecordRefresher;
15871
16083
  this.networkWorkerPool = new AsyncWorkerPool(this.concurrency);
15872
16084
  this.useBatchGQL = ldsPrimingGraphqlBatch.isOpen({ fallback: false });
16085
+ if (this.useBatchGQL) {
16086
+ this.batchSize = this.batchSize / DEFAULT_GQL_QUERY_BATCH_SIZE;
16087
+ }
15873
16088
  this.conflictPool = new ConflictPool(config.store, this.objectInfoLoader);
15874
16089
  }
15875
16090
  // function that enqueues priming work
@@ -16731,4 +16946,4 @@ register({
16731
16946
  });
16732
16947
 
16733
16948
  export { getRuntime, registerReportObserver, reportGraphqlQueryParseError };
16734
- // version: 1.228.1-4e6356f71
16949
+ // version: 1.229.0-dev10-bc9ef2513