text: ﻿/*
   * Breeze Labs SharePoint 2013 OData DataServiceAdapter
   *
   *  v.0.6.0
   *
   * Registers a SharePoint 2013 OData DataServiceAdapter with Breeze
   * 
   * REQUIRES breeze.labs.dataservice.abstractrest.js v.0.6.0+
   * 
   * This adapter cannot get metadata from the server and in general one should
   * not do so because such metadata cover much more of than you want and are huge (>1MB)
   * Better to define the metadata "by hand" on the client.
   * 
   * W/o need to get metadata, can use AJAX adapter instead of Data.JS.
   *
   * Typical usage in Angular
   *    // configure breeze to use SharePoint OData service
   *    var dsAdapter = breeze.config.initializeAdapterInstance('dataService', 'SharePointOData', true); 
   *
   *    // provide method returning value for the SP OData 'X-RequestDigest' header
   *    dsAdapter.getRequestDigest = function(){return securityService.requestDigest}
   *
   * This adapter has its own JsonResultsAdapter which you could replace.
   * 
   * The dataservice adapter looks for clientTypeNameToServer and serverTypeNameToClient methods
   * on the JsonResultsAdapter during transformation of entity data to/from the server
   * so it can convert server type names to client EntityType names.
   * These are initialized to the default versions defined here. 
   * You can create and attach alternative type name conversion methods to the JsonResultsAdapter
   * 
   * This adapter memoizes the type names it encounters 
   * by adding a 'typeMap' object to the JsonResultsAdapter.
   *
   * By default this adapter permits multiple entities to be saved at a time,
   * each in a separate request that this adapter fires off in parallel. 
   * and waits for all to complete.
   * 
   * If 'saveOnlyOne' == true, the adapter throws an exception
   * when asked to save more than one entity at a time.
   * 
   * Copyright 2014 IdeaBlade, Inc.  All Rights Reserved.
   * Licensed under the MIT License
   * http://opensource.org/licenses/mit-license.php
   * Authors: Ward Bell, Andrew Connell
   */
  (function (definition, window) {
      if (window.breeze) {
          definition(window.breeze);
      } else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
          // CommonJS or Node
          var b = require('breeze');
          definition(b);
      } else if (typeof define === "function" && define["amd"] && !window.breeze) {
          // Requirejs / AMD 
          define(['breeze'], definition);
      } else {
          throw new Error("Can't find breeze");
      }
  }(function (breeze) {
      "use strict";
  
      var ctor = function () {
          this.name = "SharePointOData";
      };
      ctor.prototype.initialize = typeInitialize;
  
      function typeInitialize() {
          // Delay setting the prototype until we're sure AbstractRestDataServiceAdapter is loaded
          var proto = breeze.AbstractRestDataServiceAdapter.prototype;
          proto = breeze.core.extend(ctor.prototype, proto);
          proto.executeQuery = executeQuery;
          proto._addToSaveContext = _addToSaveContext;
          proto._createChangeRequest = _createChangeRequest;
          proto._createErrorFromResponse = _createErrorFromResponse;
          proto._createJsonResultsAdapter = _createJsonResultsAdapter;
          proto._getResponseData = _getResponseData;
          proto._processSavedEntity = _processSavedEntity;
  
          this.initialize(); // the revised initialize()
      }
  
      breeze.config.registerAdapter("dataService", ctor);
  
      function _addToSaveContext(saveContext) {
          saveContext.requestDigest = this.getRequestDigest ? this.getRequestDigest() : null;
      }
  
      function applyDefaultSelect(mappingContext) {
          var query = mappingContext.query;
          var entityType = mappingContext.entityType;
  
          // get the default select if query lacks a select and 
          // the result type is known and it has a defaultSelect
          var defaultSelect = !query.selectClause && entityType &&
              entityType.custom && entityType.custom.defaultSelect;
          if (defaultSelect) {
              // revise query with a new query that has the default select
              mappingContext.query = query.select(defaultSelect);
          }
      }
  
      function clientTypeNameToServerDefault(clientTypeName) {
          return 'SP.Data.' + clientTypeName + 'sListItem';
      }
  
      function _createChangeRequest(saveContext, entity, index) {
          var adapter = saveContext.adapter;
          var data, rawEntity, request;
          var entityManager = saveContext.entityManager;
          var helper = entityManager.helper;
          var tempKeys = saveContext.tempKeys;
  
          var aspect = entity.entityAspect;
          var state = aspect.entityState;
          var type = entity.entityType;
          var headers = {
              'Accept': 'application/json;odata=verbose',
              'Content-Type': 'application/json;odata=verbose',
              'DataServiceVersion': '2.0', // or get MIME type error
              'X-RequestDigest': saveContext.requestDigest
          };
  
          if (state.isAdded()) {
              var rn = type.defaultResourceName;
              if (!rn) {
                  throw new Error("Missing defaultResourceName for type " + type.name);
              }
              if (type.autoGeneratedKeyType !== breeze.AutoGeneratedKeyType.None) {
                  tempKeys[index] = aspect.getKey(); // DO NOT PUSH. Gaps expected!
              }
  
              rawEntity = helper.unwrapInstance(entity, adapter._transformSaveValue);
              rawEntity.__metadata = {
                  'type': adapter._clientTypeNameToServer(type.shortName)
              };
  
              data = adapter._serializeToJson(rawEntity);
              request = {
                  requestUri: entityManager.dataService.serviceName + rn,
                  method: "POST",
                  data: data
              };
  
          } else if (state.isDeleted()) {
              request = {
                  method: "DELETE",
                  data: null
              };
              adjustUpdateDeleteRequest();
  
          } else if (state.isModified()) {
              request = {
                  method: "POST"
              };
              adjustUpdateDeleteRequest();
              headers['X-HTTP-Method'] = 'MERGE';
              rawEntity = helper.unwrapChangedValues(
                  entity, entityManager.metadataStore, adapter._transformSaveValue);
              // Todo: determine if SharePoint really wants this __metadata
              rawEntity.__metadata = { 'type': aspect.extraMetadata.type };
              request.data = adapter._serializeToJson(rawEntity);
  
          } else {
              throw new Error("Cannot save an entity whose EntityState is " + state.name);
          }
  
          request.headers = headers;
  
          return request;
  
          function adjustUpdateDeleteRequest() {
              var extraMetadata = aspect.extraMetadata;
              if (!extraMetadata) {
                  throw new Error("Missing the extra metadata for an update/delete entity");
              }
              var uri = extraMetadata.uri || extraMetadata.id;
              request.requestUri = uri;
              if (extraMetadata.etag) {
                  headers["If-Match"] = extraMetadata.etag;
              }
              return request;
          }
      }
  
      // Create error object for both query and save responses.
      // 'context' can help differentiate query and save
      // 'errorEntity' only defined for save response
      function _createErrorFromResponse(response, url, context, errorEntity) {
          // OData errors can have the message buried very deeply - and nonobviously
          // this code is tricky so be careful changing the response.body parsing.
          var err = new Error();
          err.response = response;
          if (url) { err.url = url; }
          err.message = response.message || response.error || response.statusText;
          err.statusText = response.statusText;
          err.status = response.status;
  
          setSPODataErrorMessage(err);
          proto._catchNoConnectionError(err);
          return err;
      }
  
      // TODO: relocate where re-usable?
      function setSPODataErrorMessage(err){
          // OData errors can have the message buried very deeply - and nonobviously
          // Normal MS OData responses have a response.body
          // SharePoint OData responses have a response.data instead
          // this code is tricky so be careful changing the response.data parsing.
          var data = err.data = err.response.data,
              m, 
              msg = [], 
              nextErr;
  
          if (data) {
              try {
                  if (typeof data === "string") {
                      data = err.data = JSON.parse(data);
                  }
                  do {
                      nextErr = data.error || data.innererror;
                      if (!nextErr) { 
                          m = data.message || "";
                          msg.push((typeof m === "string") ? m : m.value); 
                      }
                      nextErr = nextErr || data.internalexception;
                      data = nextErr;
                  } while (nextErr);
                  if (msg.length > 0) {
                      err.message = msg.join('; ')+'.';
                  }
              } catch (e) { /* carry on */ }
          }
      }
  
      function _createJsonResultsAdapter() {
  
          var dataServiceAdapter = this;
          var jsonResultsAdapter = new breeze.JsonResultsAdapter({
              name: dataServiceAdapter.name + "_default",
              visitNode: visitNode
          });
  
          jsonResultsAdapter.clientTypeNameToServer = clientTypeNameToServerDefault;
          jsonResultsAdapter.serverTypeNameToClient = serverTypeNameToClientDefault;
  
          return jsonResultsAdapter;
  
          function visitNode(node, mappingContext, nodeContext) {
              var result = { ignore: true };
              if (!node) { return result; }
  
              var propertyName = nodeContext.propertyName;
              var ignore = node.__deferred != null || propertyName === "__metadata" ||
                  // EntityKey properties can be produced by EDMX models
                  (propertyName === "EntityKey" && node.$type && core.stringStartsWith(node.$type, "System.Data"));
  
              if (!ignore) {
                  result = {};
                  updateEntityNode(node, mappingContext, result);
  
                  // OData v3 - projection arrays will be enclosed in a results array
                  if (node.results) {
                      result.node = node.results;
                  }
              }
              return result;
          }
  
          // Determine if this is an Entity node and update the node appropriately if so
          function updateEntityNode(node, mappingContext, result) {
              var metadata = node.__metadata;
              if (!metadata) { return; } // every SharePoint entity node has __metadata
  
              var entityType = node.$entityType; 
              if (entityType){
                  // save result node
                  result.entityType = entityType;
                  result.extraMetadata = metadata;
                  return;
              }            
  
              // query node
  
              var typeName = dataServiceAdapter._serverTypeNameToClient(mappingContext, metadata.type);
              entityType = dataServiceAdapter._getNodeEntityType(mappingContext, typeName);
  
              if (entityType) {
                  // ASSUME if #-of-properties on node is >= #-of-props for the type 
                  // that this is the full entity and not a partial projection. 
                  // Therefore we declare that we've received an entity 
                  if (entityType._mappedPropertiesCount <= Object.keys(node).length - 1) {
                      result.entityType = entityType;
                      result.extraMetadata = metadata;
  
                      // Delete node properties that look like nested navigation paths
                      // Breeze gets confused into thinking such properties contain actual entities. 
                      // Todo: rethink this if/when can include related entities through expand
                      var navPropNames = entityType.navigationProperties.map(function (p) { return p.name; });
                      navPropNames.forEach(function (n) { if (node[n]) { delete node[n]; } });
                  }
              }
          }
      }
  
      function executeQuery(mappingContext) {
          var adapter = this;
          mappingContext.entityType = adapter._getEntityTypeFromMappingContext(mappingContext);
          applyDefaultSelect(mappingContext);
          var deferred = adapter.Q.defer();
          var url = mappingContext.getUrl();
          var headers = {
              'Accept': 'application/json;odata=verbose',
              'DataServiceVersion': '2.0' // seems to work w/o this but just in case
          };
  
          adapter._ajaxImpl.ajax({
              type: "GET",
              url: url,
              headers: headers,
              params: mappingContext.query.parameters,
              success: querySuccess,
              error: function (response) {
                  deferred.reject(adapter._createErrorFromResponse(response, url));
              }
          });
          return deferred.promise;
  
          function querySuccess(response) {
              try {
                  var data = response.data;
                  var inlineCount = data.__count ? parseInt(data.__count, 10) : undefined;
                  var rData = {
                      results: adapter._getResponseData(response).results,
                      inlineCount: inlineCount,
                      httpResponse: response
                  };
                  deferred.resolve(rData);
              } catch (e) {
                  // program error means adapter it broken, not SP or the user
                  deferred.reject(new Error("Program error: failed while parsing successful query response"));
              }
          }
      }
  
      function _getResponseData(response) {
          return response.data && response.data.d; // sharepoint adds a 'd' !?!
      }
  
      function _processSavedEntity(savedEntity, response /*, saveContext, index*/) {
          var etag = savedEntity && savedEntity.entityAspect && response.getHeaders('ETag');
          if (etag) {
              savedEntity.entityAspect.extraMetadata.etag = etag;
          }
      }
  
      function serverTypeNameToClientDefault(serverTypeName) {
          // strip off leading 'SP.Data.' and trailing 'sListItem'
          var re = /^(SP\.Data.)(.*)(sListItem)$/;
          var typeName = serverTypeName.replace(re, '$2');
  
          return breeze.MetadataStore.normalizeTypeName(typeName);
      }
  
  }, this));