breeze-client-labs
Version:
Breeze Labs are extensions and utilities for Breeze.js client apps that are not part of core breeze.
460 lines (402 loc) • 18.6 kB
JavaScript
/*
* Breeze Labs Abstract REST DataServiceAdapter
*
* v.0.6.6
*
* Extends Breeze with a REST DataService Adapter abstract type
*
* N.B.: This adapter CANNOT be used directly!
*
* It's a base type for concrete REST adapters such as the SharePoint OData DataService Adapter
* and the Azure Mobile Services adapter
*
* A concrete REST adapter
*
* - MUST replace the _createChangeRequest with a concrete implementation to enable save
*
* - SHOULD replace the "noop" JsonResultsAdapter.
*
* - WILL LIKELY replace the executeQuery method.
*
* - COULD replace the fetchMetadata method and MUST do so if getting metadata from the server.
*
* - MAY replace any of the protected members prefixed by '_'.
*
* FOR EXAMPLE IMPLEMENTATION, SEE breeze.labs.dataservice.sharepoint.js
*
* 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 2015 IdeaBlade, Inc. All Rights Reserved.
* Licensed under the MIT License
* http://opensource.org/licenses/mit-license.php
* Authors: Ward Bell
*/
(function (definition) {
if (typeof breeze === "object") {
definition(breeze);
} else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
// CommonJS or Node
var b = require('breeze-client');
definition(b);
} else if (typeof define === "function" && define["amd"]) {
// Requirejs / AMD
define(['breeze-client'], definition);
} else {
throw new Error("Can't find breeze");
}
}(function (breeze) {
"use strict";
var ctor = function () { };
breeze.AbstractRestDataServiceAdapter = ctor;
// borrow from the AbstractDataServiceAdapter
var abstractDsaProto = breeze.AbstractDataServiceAdapter.prototype;
ctor.prototype = {
// Breeze DataService API
executeQuery: executeQuery,
fetchMetadata: fetchMetadata,
initialize: initialize,
saveChanges: saveChanges,
// Configuration API
changeRequestInterceptor: abstractDsaProto.changeRequestInterceptor, // default, no-op ctor
checkForRecomposition: checkForRecomposition,
saveOnlyOne: false, // true if may only save one entity at a time.
ignoreDeleteNotFound: true, // true if should ignore a 404 error from a delete
// "protected" members available to derived concrete dataservice adapter types
_addToSaveContext: _addToSaveContext,
_addKeyMapping: _addKeyMapping,
_ajaxImpl: undefined, // see initialize()
_catchNoConnectionError: abstractDsaProto._catchNoConnectionError,
_createChangeRequestInterceptor: abstractDsaProto._createChangeRequestInterceptor,
_changeRequestSucceeded: _changeRequestSucceeded,
_createErrorFromResponse: _createErrorFromResponse,
_createChangeRequest: _createChangeRequest,
_createJsonResultsAdapter: _createJsonResultsAdapter,
_clientTypeNameToServer: _clientTypeNameToServer,
_getEntityTypeFromMappingContext: _getEntityTypeFromMappingContext,
_getNodeEntityType: _getNodeEntityType,
_getResponseData: _getResponseData,
_processSavedEntity: _processSavedEntity,
_serializeToJson: _serializeToJson, // serialize raw entity data to JSON for save
_serverTypeNameToClient: _serverTypeNameToClient,
_transformSaveValue: _transformSaveValue
};
/*** Breeze DataService API ***/
function initialize() {
var adapter = this;
var ajaxImpl = adapter._ajaxImpl = breeze.config.getAdapterInstance("ajax");
if (!ajaxImpl) {
throw new Error("Unable to initialize ajax for " + adapter.name);
}
var ajax = ajaxImpl.ajax;
if (!ajax) {
throw new Error("Breeze was unable to find an 'ajax' adapter for " + adapter.name);
}
adapter.Q = breeze.Q; // adapter.Q is for backward compat
if (!adapter.jsonResultsAdapter) {
adapter.jsonResultsAdapter = adapter._createJsonResultsAdapter();
}
}
function checkForRecomposition(interfaceInitializedArgs) {
if (interfaceInitializedArgs.interfaceName === "ajax" && interfaceInitializedArgs.isDefault) {
this.initialize();
}
}
function executeQuery(mappingContext) {
var adapter = mappingContext.adapter = this;
var deferred = adapter.Q.defer();
var url = mappingContext.getUrl();
var headers = {
'Accept': 'application/json'
};
adapter._ajaxImpl.ajax({
type: "GET",
url: url,
headers: headers,
params: mappingContext.query.parameters,
success: querySuccess,
error: function (response) {
deferred.reject(adapter._createErrorFromResponse(response, url, mappingContext));
}
});
return deferred.promise;
function querySuccess(response) {
try {
var rData = {
results: adapter._getResponseData(response),
httpResponse: response
};
deferred.resolve(rData);
} catch (e) {
// if here, the adapter is broken, not bad data
var err = new Error("Query failed while parsing successful query response")
err.name = "Program Error";
err.response = response;
err.originalError = e;
deferred.reject(err);
}
}
}
function fetchMetadata() {
throw new Error("Cannot process server metadata; create your own and use that instead");
}
function saveChanges(saveContext, saveBundle) {
var adapter = saveContext.adapter = this;
var Q = adapter.Q;
try {
if (adapter.saveOnlyOne && saveBundle.entities.length > 1) {
return Q.reject(new Error("Only one entity may be saved at a time."));
}
adapter._addToSaveContext(saveContext);
var requests = createChangeRequests(saveContext, saveBundle);
var promises = sendChangeRequests(saveContext, requests);
var comboPromise = Q.all(promises);
return comboPromise
.then(reviewSaveResult)
.then(null, saveFailed);
} catch (err) {
return Q.reject(err);
}
function reviewSaveResult(/* promiseValues */) {
var saveResult = saveContext.saveResult;
var entitiesWithErrors = saveResult.entitiesWithErrors;
var errorCount = entitiesWithErrors.length;
if (!errorCount) { return saveResult; } // all good
// at least one request failed; process those that succeeded
saveContext.processSavedEntities(saveResult);
var error;
// Compose error; promote the first error when one or all fail
if (requests.length === 1 || requests.length === errorCount) {
// When all fail, good chance the first error is the same reason for all
error = entitiesWithErrors[0].error;
} else {
error = new Error("\n The save failed although some entities were saved.");
}
error.message = (error.message || "Save failed") +
" \n See 'error.saveResult' for more details.\n";
error.saveResult = saveResult;
return Q.reject(error);
}
function saveFailed(error) {
return Q.reject(error);
}
}
/*** Members a derived Type might use or replace ***/
function _addToSaveContext(/* saveContext */) { }
function _addKeyMapping(saveContext, index, saved) {
var tempKey = saveContext.tempKeys[index];
if (tempKey) {
// entity had a temporary key; add a temp-to-perm key mapping
var entityType = tempKey.entityType;
var tempValue = tempKey.values[0];
var realKey = getRealKey(entityType, saved);
var keyMapping = {
entityTypeName: entityType.name,
tempValue: tempValue,
realValue: realKey.values[0]
};
saveContext.saveResult.keyMappings.push(keyMapping);
}
}
function _clientTypeNameToServer(typeName) {
var jrAdapter = this.jsonResultsAdapter;
return jrAdapter.clientTypeNameToServer ?
jrAdapter.clientTypeNameToServer(typeName) : typeName;
}
function _createChangeRequest(/* saveContext, entity, index */) {
throw new Error("Need a concrete implementation of _createChangeRequest");
}
// Create error object for both query and save responses.
// A method on the adapter (`this`)
// 'context' can help differentiate query and save
// 'errorEntity' only defined for save response
function _createErrorFromResponse(response, url, context, errorEntity) {
var err = new Error();
err.response = response;
var data = response.data || {};
if (url) { err.url = url; }
err.status = data.code || response.status || '???';
err.statusText = response.statusText || err.status;
err.message = data.error || response.message || response.error || err.statusText;
this._catchNoConnectionError(err);
return err;
}
function _createJsonResultsAdapter(/*dataServiceAdapter*/) {
return new breeze.JsonResultsAdapter({
name: "noop",
visitNode: function (/*node, mappingContext, nodeContext*/) {
return {};
}
});
}
function _getEntityTypeFromMappingContext(mappingContext) {
var query = mappingContext.query;
if (!query) { return null; }
var entityType = query.entityType || query.resultEntityType;
if (!entityType) { // try to figure it out from the query.resourceName
var metadataStore = mappingContext.metadataStore;
var etName = metadataStore.getEntityTypeNameForResourceName(query.resourceName);
if (etName) {
entityType = metadataStore.getEntityType(etName);
}
}
return entityType;
}
function _getNodeEntityType(mappingContext, typeName) {
// Get the EntityType corresponding to the typeName
// A utility for implementation of jsonResultsAdapter.visitNode
// typeName: a string on the node that identifies the type of the raw data
//
// This method memoizes the type names it encounters
// by adding a 'typeMap' object to the JsonResultsAdapter.
if (!typeName) { return undefined; }
var jsonResultsAdapter = mappingContext.jsonResultsAdapter;
var typeMap = jsonResultsAdapter.typeMap;
if (!typeMap) { // if missing, make one with a fallback mapping
typeMap = { "": { _mappedPropertiesCount: NaN } };
jsonResultsAdapter.typeMap = typeMap;
}
var entityType = typeMap[typeName]; // EntityType for a node with this metadata.type
if (!entityType) {
// Haven't see this typeName before; add it to the typeMap
// Figure out what EntityType this is and remember it
entityType = mappingContext.metadataStore.getEntityType(typeName, true);
typeMap[typeName] = entityType || typeMap[""];
}
return entityType;
}
function _getResponseData(response) {
return response.data;
}
function _processSavedEntity(/*savedEntity, response, saveContext, index*/) {
// Virtual method. Override in concrete adapter if needed.
}
function _serializeToJson(rawEntityData) {
// Serialize raw entity data to JSON during save
// You could override this default version
// Note that DataJS has an amazingly complex set of tricks for this,
// all of them depending on metadata attached to the property values
// which breeze entity data never have.
return JSON.stringify(rawEntityData);
}
function _serverTypeNameToClient(mappingContext, typeName) {
var jrAdapter = mappingContext.jsonResultsAdapter;
return jrAdapter.serverTypeNameToClient ?
jrAdapter.serverTypeNameToClient(typeName) : typeName;
}
function _transformSaveValue(prop, val) {
// prepare a property value for save by transforming it
if (prop.isUnmapped) { return undefined; }
if (prop.dataType === breeze.DataType.DateTimeOffset) {
// The datajs lib tries to treat client dateTimes that are defined as DateTimeOffset on the server differently
// from other dateTimes. This fix compensates before the save.
// TODO: If not using datajs (and this adapter doesn't) is this necessary?
val = val && new Date(val.getTime() - (val.getTimezoneOffset() * 60000));
} else if (prop.dataType.quoteJsonOData) {
val = val != null ? val.toString() : val;
}
return val;
}
/*** private members ***/
function createChangeRequests(saveContext, saveBundle) {
var adapter = saveContext.adapter;
var originalEntities = saveContext.originalEntities = saveBundle.entities;
saveContext.tempKeys = [];
var changeRequestInterceptor =
adapter._createChangeRequestInterceptor(saveContext, saveBundle);
var requests = originalEntities.map(function (entity, index) {
var request = adapter._createChangeRequest(saveContext, entity, index);
return changeRequestInterceptor.getRequest(request, entity, index);
});
changeRequestInterceptor.done(requests);
return requests;
}
function getRealKey(entityType, rawEntity) {
return entityType.getEntityKeyFromRawEntity(rawEntity,
breeze.DataProperty.getRawValueFromServer);
}
function sendChangeRequests(saveContext, requests) {
// Sends each prepared save request and processes the promised results
// returns a single "comboPromise" that waits for the individual promises to complete
// Todo: What happens when there are a gazillion async requests?
var saveResult = {
entities: [],
entitiesWithErrors: [],
keyMappings: []
};
saveContext.saveResult = saveResult;
return requests.map(function (request, index) {
return sendChangeRequest(saveContext, request, index);
});
}
function sendChangeRequest(saveContext, request, index) {
var adapter = saveContext.adapter;
var deferred = adapter.Q.defer();
var url = request.requestUri;
adapter._ajaxImpl.ajax({
url: url,
type: request.method,
headers: request.headers,
data: request.data,
success: tryRequestSucceeded,
error: tryRequestFailed
});
return deferred.promise;
function tryRequestSucceeded(response) {
try {
var status = +response.status;
if ((!status) || status >= 400) {
tryRequestFailed(response);
} else {
var savedEntity = adapter._changeRequestSucceeded(saveContext, response, index);
adapter._processSavedEntity(savedEntity, response, saveContext, index);
deferred.resolve(true);
}
} catch (e) {
// program error means adapter is broken, not remote server or the user
deferred.reject("Program error: failed while processing successful save response");
}
}
function tryRequestFailed(response) {
try {
var status = +response.status;
if (status && status === 404 && adapter.ignoreDeleteNotFound &&
saveContext.originalEntities[index].entityAspect.entityState.isDeleted()) {
// deleted entity not found; treat as if successfully deleted.
response.status = 204;
response.statusText = 'resource was already deleted; no content';
response.data = undefined;
tryRequestSucceeded(response);
} else {
// Do NOT fail saveChanges at the request level
var errorEntity = saveContext.originalEntities[index];
saveContext.saveResult.entitiesWithErrors.push({
entity: errorEntity,
error: adapter._createErrorFromResponse(response, url, saveContext, errorEntity)
});
deferred.resolve(false);
}
} catch (e) {
// program error means adapter is broken, not remote server or the user
deferred.reject("Program error: failed while processing save error");
}
}
}
function _changeRequestSucceeded(saveContext, response, index) {
var saved = saveContext.adapter._getResponseData(response);
if (saved && typeof saved === 'object') {
// Have "saved entity" data; add its type (for JsonResultsAdapter) & KeyMapping
saved.$entityType = saveContext.originalEntities[index].entityType;
saveContext.adapter._addKeyMapping(saveContext, index, saved);
} else {
// No "saved entity" data; return the original entity
saved = saveContext.originalEntities[index];
}
saveContext.saveResult.entities.push(saved);
return saved;
}
}));