Show:

File: a55_mappingContext.js

/**
 @module breeze
 **/

// Internal helper class

var MappingContext = (function () {

  var ctor = function MappingContext(config) {

    __extend(this, config, [
      "query", "entityManager", "dataService", "mergeOptions"
    ]);

    // calc'd props
    this.refMap = {};
    this.deferredFns = [];
    this.jsonResultsAdapter = this.dataService.jsonResultsAdapter;
    this.metadataStore = this.entityManager.metadataStore;
    this.rawValueFn = DataProperty.getRawValueFromServer; // think about passing this in later.
  };

  var proto = ctor.prototype;
  var parseRawValue = DataType.parseRawValue;
  proto._$typeName = "MappingContext";

  proto.getUrl = function () {
    var query = this.query;
    if (!query) {
      throw new Error("query cannot be empty");
    }
    var uriString;
    if (typeof query === 'string') {
      uriString = query;
    } else if (query instanceof EntityQuery) {
      uriString = this.dataService.uriBuilder.buildUri(query, this.metadataStore);
    } else {
      throw new Error("unable to recognize query parameter as either a string or an EntityQuery");
    }
    return  this.dataService.qualifyUrl(uriString);
  }

  proto.visitAndMerge = function (nodes, nodeContext) {
    var query = this.query;
    var jra = this.jsonResultsAdapter;
    nodeContext = nodeContext || {};
    var that = this;
    return __map(nodes, function (node) {
      if (query == null && node.entityAspect) {
        // don't bother merging a result from a save that was not returned from the server.
        if (node.entityAspect.entityState.isDeleted()) {
          that.entityManager.detachEntity(node);
        } else {
          node.entityAspect.acceptChanges();
        }
        return node;
      }

      var meta = jra.visitNode(node, that, nodeContext) || {};
      node = meta.node || node;
      if (query && nodeContext.nodeType === "root" && !meta.entityType) {
        meta.entityType = query._getToEntityType && query._getToEntityType(that.metadataStore);
      }
      return processMeta(that, node, meta);
    }, this.mergeOptions.includeDeleted);
  };

  proto.processDeferred = function () {
    if (this.deferredFns.length > 0) {
      this.deferredFns.forEach(function (fn) {
        fn();
      });
    }
  }

  function processMeta(mc, node, meta, assignFn) {
    // == is deliberate here instead of ===
    if (meta.ignore || node == null) {
      return null;
    } else if (meta.nodeRefId) {
      var refValue = resolveEntityRef(mc, meta.nodeRefId);
      if (typeof refValue === "function" && assignFn != null) {
        mc.deferredFns.push(function () {
          assignFn(refValue);
        });
        return undefined; // deferred and will be set later;
      }
      return refValue;
    } else if (meta.entityType) {
      var entityType = meta.entityType;
      if (mc.mergeOptions.noTracking) {
        node = processNoMerge(mc, entityType, node);
        if (entityType.noTrackingFn) {
          node = entityType.noTrackingFn(node, entityType);
        }
        if (meta.nodeId) {
          mc.refMap[meta.nodeId] = node;
        }
        return node;
      } else {
        if (entityType.isComplexType) {
          // because we still need to do serverName to client name processing
          return processNoMerge(mc, entityType, node);
        } else {
          return mergeEntity(mc, node, meta);
        }
      }
    } else {

      if ((!meta.passThru) && typeof node === 'object' && !__isDate(node)) {
        node = processAnonType(mc, node);
      }

      // updating the refMap for entities is handled by updateEntityRef for entities.
      if (meta.nodeId) {
        mc.refMap[meta.nodeId] = node;
      }
      return node;
    }
  }

  function processNoMerge(mc, stype, node) {
    var result = {};

    stype.dataProperties.forEach(function (dp) {
      if (dp.isComplexProperty) {
        result[dp.name] = __map(node[dp.nameOnServer], function (v) {
          return processNoMerge(mc, dp.dataType, v);
        });
      } else {
        result[dp.name] = parseRawValue(node[dp.nameOnServer], dp.dataType);
      }
    });

    stype.navigationProperties && stype.navigationProperties.forEach(function (np) {
      var nodeContext = { nodeType: "navProp", navigationProperty: np };
      visitNode(node[np.nameOnServer], mc, nodeContext, result, np.name);
    });

    return result;
  }

  function processAnonType(mc, node) {
    // node is guaranteed to be an object by this point, i.e. not a scalar
    var keyFn = mc.metadataStore.namingConvention.serverPropertyNameToClient;
    var result = {};

    __objectForEach(node, function (key, value) {
      var newKey = keyFn(key);
      var nodeContext = { nodeType: "anonProp", propertyName: newKey };
      visitNode(value, mc, nodeContext, result, newKey);
    });
    return result;
  }

  function visitNode(node, mc, nodeContext, result, key) {
    var jra = mc.jsonResultsAdapter;
    var meta = jra.visitNode(node, mc, nodeContext) || {};
    // allows visitNode to change the value;
    node = meta.node || node;

    if (meta.ignore) return;
    if (meta.passThru) return node;
    if (Array.isArray(node)) {
      nodeContext.nodeType = nodeContext.nodeType + "Item";
      result[key] = node.map(function (v, ix) {
        meta = jra.visitNode(v, mc, nodeContext) || {};
        v = meta.node || v;
        return processMeta(mc, v, meta, function (refValue) {
          result[key][ix] = refValue();
        });
      });
    } else {
      result[key] = processMeta(mc, node, meta, function (refValue) {
        result[key] = refValue();
      });
    }
  }

  function resolveEntityRef(mc, nodeRefId) {
    var entity = mc.refMap[nodeRefId];
    if (entity === undefined) {
      return function () {
        return mc.refMap[nodeRefId];
      };
    } else {
      return entity;
    }
  }

  function updateEntityRef(mc, targetEntity, node) {
    var nodeId = node._$meta.nodeId;
    if (nodeId != null) {
      mc.refMap[nodeId] = targetEntity;
    }
  }

  // can return null for a deleted entity if includeDeleted == false
  function mergeEntity(mc, node, meta) {
    node._$meta = meta;
    var em = mc.entityManager;

    var entityType = meta.entityType;
    if (typeof (entityType) === 'string') {
      entityType = mc.metadataStore._getEntityType(entityType, false);
    }
    node.entityType = entityType;

    var mergeStrategy = mc.mergeOptions.mergeStrategy;
    var isSaving = mc.query == null;

    var entityKey = entityType.getEntityKeyFromRawEntity(node, mc.rawValueFn);
    var targetEntity = em.findEntityByKey(entityKey);
    if (targetEntity) {
      if (isSaving && targetEntity.entityAspect.entityState.isDeleted()) {
        em.detachEntity(targetEntity);
        return targetEntity;
      }
      var targetEntityState = targetEntity.entityAspect.entityState;
      if (mergeStrategy === MergeStrategy.Disallowed) {
        throw new Error("A MergeStrategy of 'Disallowed' prevents " + entityKey.toString() + " from being merged");
      } else if (mergeStrategy === MergeStrategy.SkipMerge) {
        updateEntityNoMerge(mc, targetEntity, node);
      } else {
        if (mergeStrategy === MergeStrategy.OverwriteChanges
            || targetEntityState.isUnchanged()) {
          updateEntity(mc, targetEntity, node);
          targetEntity.entityAspect.wasLoaded = true;
          if (meta.extraMetadata) {
            targetEntity.entityAspect.extraMetadata = meta.extraMetadata;
          }
          targetEntity.entityAspect.entityState = EntityState.Unchanged;
          targetEntity.entityAspect.originalValues = {};
          targetEntity.entityAspect.propertyChanged.publish({ entity: targetEntity, propertyName: null });
          var action = isSaving ? EntityAction.MergeOnSave : EntityAction.MergeOnQuery;
          em.entityChanged.publish({ entityAction: action, entity: targetEntity });
          // this is needed to handle an overwrite of a modified entity with an unchanged entity
          // which might in turn cause _hasChanges to change.
          if (!targetEntityState.isUnchanged()) {
            em._notifyStateChange(targetEntity, false);
          }
        } else {
          if (targetEntityState == EntityState.Deleted && !mc.mergeOptions.includeDeleted) {
            return null;
          }
          updateEntityNoMerge(mc, targetEntity, node);
        }
      }
    } else {
      targetEntity = entityType._createInstanceCore();

      updateEntity(mc, targetEntity, node);

      if (meta.extraMetadata) {
        targetEntity.entityAspect.extraMetadata = meta.extraMetadata;
      }
      // em._attachEntityCore(targetEntity, EntityState.Unchanged, MergeStrategy.Disallowed);
      em._attachEntityCore(targetEntity, EntityState.Unchanged, mergeStrategy);
      targetEntity.entityAspect.wasLoaded = true;
      em.entityChanged.publish({ entityAction: EntityAction.AttachOnQuery, entity: targetEntity });
    }
    return targetEntity;
  }

  function updateEntityNoMerge(mc, targetEntity, node) {
    updateEntityRef(mc, targetEntity, node);
    // we still need to merge related entities even if top level entity wasn't modified.
    node.entityType.navigationProperties.forEach(function (np) {
      if (np.isScalar) {
        mergeRelatedEntityCore(mc, node, np);
      } else {
        mergeRelatedEntitiesCore(mc, node, np);
      }
    });
  }

  function updateEntity(mc, targetEntity, node) {
    updateEntityRef(mc, targetEntity, node);
    var entityType = targetEntity.entityType;
    entityType._updateTargetFromRaw(targetEntity, node, mc.rawValueFn);

    entityType.navigationProperties.forEach(function (np) {
      if (np.isScalar) {
        mergeRelatedEntity(mc, np, targetEntity, node);
      } else {
        mergeRelatedEntities(mc, np, targetEntity, node);
      }
    });
  }

  function mergeRelatedEntity(mc, navigationProperty, targetEntity, rawEntity) {

    var relatedEntity = mergeRelatedEntityCore(mc, rawEntity, navigationProperty);
    if (relatedEntity == null) return;
    if (typeof relatedEntity === 'function') {
      mc.deferredFns.push(function () {
        relatedEntity = relatedEntity();
        updateRelatedEntity(relatedEntity, targetEntity, navigationProperty);
      });
    } else {
      updateRelatedEntity(relatedEntity, targetEntity, navigationProperty);
    }
  }

  function mergeRelatedEntities(mc, navigationProperty, targetEntity, rawEntity) {
    var relatedEntities = mergeRelatedEntitiesCore(mc, rawEntity, navigationProperty);
    if (relatedEntities == null) return;

    var inverseProperty = navigationProperty.inverse;
    if (!inverseProperty) return;

    var originalRelatedEntities = targetEntity.getProperty(navigationProperty.name);
    originalRelatedEntities.wasLoaded = true;

    relatedEntities.forEach(function (relatedEntity) {
      if (typeof relatedEntity === 'function') {
        mc.deferredFns.push(function () {
          relatedEntity = relatedEntity();
          updateRelatedEntityInCollection(mc, relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
        });
      } else {
        updateRelatedEntityInCollection(mc, relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
      }
    });
  }

  function mergeRelatedEntityCore(mc, rawEntity, navigationProperty) {
    var relatedRawEntity = rawEntity[navigationProperty.nameOnServer];
    if (!relatedRawEntity) return null;

    var relatedEntity = mc.visitAndMerge(relatedRawEntity, { nodeType: "navProp", navigationProperty: navigationProperty });
    return relatedEntity;
  }

  function mergeRelatedEntitiesCore(mc, rawEntity, navigationProperty) {
    var relatedRawEntities = rawEntity[navigationProperty.nameOnServer];
    if (!relatedRawEntities) return null;

    // needed if what is returned is not an array and we expect one - this happens with __deferred in OData.
    if (!Array.isArray(relatedRawEntities)) {
      // return null;
      relatedRawEntities = relatedRawEntities.results; // OData v3 will look like this with an expand
      if (!relatedRawEntities) {
        return null;
      }
    }

    var relatedEntities = mc.visitAndMerge(relatedRawEntities, { nodeType: "navPropItem", navigationProperty: navigationProperty });
    return relatedEntities;
  }

  function updateRelatedEntity(relatedEntity, targetEntity, navigationProperty) {
    if (!relatedEntity) return;
    var propName = navigationProperty.name;
    var currentRelatedEntity = targetEntity.getProperty(propName);

    // check if the related entity is already hooked up
    if (currentRelatedEntity !== relatedEntity) {
      // if not hook up both directions.
      targetEntity.setProperty(propName, relatedEntity);
      var inverseProperty = navigationProperty.inverse;
      if (!inverseProperty) return;
      if (inverseProperty.isScalar) {
        relatedEntity.setProperty(inverseProperty.name, targetEntity);
      } else {
        var collection = relatedEntity.getProperty(inverseProperty.name);
        collection.push(targetEntity);

      }
    }
  }

  function updateRelatedEntityInCollection(mc, relatedEntity, relatedEntities, targetEntity, inverseProperty) {
    if (!relatedEntity) return;

    // don't update relatedCollection if preserveChanges & relatedEntity has an fkChange.
    if (relatedEntity.entityAspect.entityState === EntityState.Modified
      && mc.mergeOptions.mergeStrategy === MergeStrategy.PreserveChanges) {
      var origValues = relatedEntity.entityAspect.originalValues;
      var fkWasModified = inverseProperty.relatedDataProperties.some(function(dp) {
        return origValues[dp.name] != undefined;
      });
      if (fkWasModified) return;
    }
    // check if the related entity is already hooked up
    var thisEntity = relatedEntity.getProperty(inverseProperty.name);

    if (thisEntity !== targetEntity) {
      // if not - hook it up.
      relatedEntities.push(relatedEntity);
      relatedEntity.setProperty(inverseProperty.name, targetEntity);
    }
  }


  return ctor;
})();