Show:

File: ..\Scripts\IBlade\a55_mappingContext.js

/**
@module breeze
**/

// Internal helper class

var MappingContext = (function () {
    
    var ctor = function (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 () {
        return  this.dataService.makeUrl(this.metadataStore.toQueryString(this.query));
    }

    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);
        });
    };

    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 (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 (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;
        }
    }

    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.extra) {
                        targetEntity.entityAspect.extraMetadata = meta.extra;
                    }
                    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 {
                    updateEntityNoMerge(mc, targetEntity, node);
                }
            }
        } else {
            targetEntity = entityType._createInstanceCore();
          
            updateEntity(mc, targetEntity, node);
            
            if (meta.extra) {
                targetEntity.entityAspect.extraMetadata = meta.extra;
            }
            em._attachEntityCore(targetEntity, EntityState.Unchanged, MergeStrategy.Disallowed);
            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;
        // Uncomment when we implement entityAspect.isNavigationPropertyLoaded method
        // targetEntity.entityAspect.markNavigationPropertyAsLoaded(navigationProperty);
        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(relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
                });
            } else {
                updateRelatedEntityInCollection(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);

        // Uncomment when we implement entityAspect.isNavigationPropertyLoaded method
        // targetEntity.entityAspect.markNavigationPropertyAsLoaded(navigationProperty);

        // 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);

                // Uncomment when we implement entityAspect.isNavigationPropertyLoaded method
                // relatedEntity.entityAspect.markNavigationPropertyAsLoaded(inverseProperty);
            } else {
                var collection = relatedEntity.getProperty(inverseProperty.name);
                collection.push(targetEntity);
                // can't call _markAsLoaded here because this may be only a partial load.
            }
        }
    } 

    function updateRelatedEntityInCollection(relatedEntity, relatedEntities, targetEntity, inverseProperty) {
        if (!relatedEntity) return;
        // Uncomment when we implement entityAspect.isNavigationPropertyLoaded method
        // relatedEntity.entityAspect.markNavigationPropertyAsLoaded(inverseProperty);
        // 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;
})();