breeze-client
Version:
Breeze data management for JavaScript clients
517 lines (512 loc) • 22.3 kB
JavaScript
import * as breeze from 'breeze-client';
/** Appends parameter to url, with appropriate delimiter */
function appendQueryStringParameter(url, parameter) {
if (!parameter)
return url;
let separator = url.endsWith("?") ? "" : url.includes("?") ? "&" : "?";
return url + separator + parameter;
}
/** Encodes the object into query string parameters */
function encodeParams(obj) {
let query = '';
let subValue, innerObj, fullSubName;
for (let name in obj) {
if (!obj.hasOwnProperty(name)) {
continue;
}
let value = obj[name];
if (value instanceof Array) {
for (let i = 0; i < value.length; ++i) {
subValue = value[i];
fullSubName = name + '[' + i + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += encodeParams(innerObj) + '&';
}
}
else if (value && value.toISOString) { // a feature of Date-like things
query += encodeURIComponent(name) + '=' + encodeURIComponent(value.toISOString()) + '&';
}
else if (value instanceof Object) {
for (let subName in value) {
if (obj.hasOwnProperty(name)) {
subValue = value[subName];
fullSubName = name + '[' + subName + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += encodeParams(innerObj) + '&';
}
}
}
else if (value === null) {
query += encodeURIComponent(name) + '=&';
}
else if (value !== undefined) {
query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
}
}
return query.length ? query.substr(0, query.length - 1) : query;
}
let core = breeze.core;
/** @hidden */
class DataServiceODataAdapter extends breeze.AbstractDataServiceAdapter {
relativeUrl;
// _catchNoConnectionError = abstractDsaProto._catchNoConnectionError;
// changeRequestInterceptor = abstractDsaProto.changeRequestInterceptor;
// _createChangeRequestInterceptor = abstractDsaProto._createChangeRequestInterceptor;
headers = { "DataServiceVersion": "2.0" };
constructor() {
super();
this.name = "OData";
}
static register(config) {
config = config || breeze.config;
config.registerAdapter("dataService", DataServiceODataAdapter);
return config.initializeAdapterInstance("dataService", "OData", true);
}
initialize() {
OData = core.requireLib("OData", "Needed to support remote OData services");
OData.jsonHandler.recognizeDates = true;
}
// Absolute URL is the default as of Breeze 1.5.5.
// To use relative URL (like pre-1.5.5), add adapterInstance.relativeUrl = true:
//
// let ds = breeze.config.initializeAdapterInstance("dataService", "webApiOData");
// ds.relativeUrl = true;
//
// To use custom url construction, add adapterInstance.relativeUrl = myfunction(dataService, url):
//
// let ds = breeze.config.initializeAdapterInstance("dataService", "webApiOData");
// ds.relativeUrl = function(dataService, url) {
// return somehowConvert(url);
// }
//
fetchMetadata(metadataStore, dataService) {
let serviceName = dataService.serviceName;
let url;
if (this.relativeUrl === true) {
url = dataService.qualifyUrl('$metadata');
}
else if (core.isFunction(this.relativeUrl)) {
url = this.relativeUrl(dataService, '$metadata');
}
else {
url = this.getAbsoluteUrl(dataService, '$metadata');
}
let mheaders = core.extend({}, this.headers);
mheaders.Accept = 'application/*; odata.metadata=full';
let promise = new Promise((resolve, reject) => {
// OData.read(url,
OData.read({
requestUri: url,
// headers: { "Accept": "application/json"}
headers: mheaders
}, function (data) {
// data.dataServices.schema is an array of schemas. with properties of
// entityContainer[], association[], entityType[], and namespace.
if (!data || !data.dataServices) {
let error = new Error("Metadata query failed for: " + url);
return reject(error);
}
let csdlMetadata = data.dataServices;
// might have been fetched by another query
if (!metadataStore.hasMetadataFor(serviceName)) {
try {
metadataStore.importMetadata(csdlMetadata);
}
catch (e) {
return reject(new Error("Metadata query failed for " + url + "; Unable to process returned metadata: " + e.message));
}
metadataStore.addDataService(dataService);
}
return resolve(csdlMetadata);
}, function (error) {
let err = createError(error, url);
err.message = "Metadata query failed for: " + url + "; " + (err.message || "");
return reject(err);
}, OData.metadataHandler);
});
return promise;
}
executeQuery(mappingContext) {
let url;
if (this.relativeUrl === true) {
url = mappingContext.getUrl();
}
else if (core.isFunction(this.relativeUrl)) {
url = this.relativeUrl(mappingContext.dataService, mappingContext.getUrl());
}
else {
url = this.getAbsoluteUrl(mappingContext.dataService, mappingContext.getUrl());
}
// Add query params if .withParameters was used
let query = mappingContext.query;
if (!core.isEmpty(query.parameters)) {
let paramString = toQueryString(query.parameters);
url = appendQueryStringParameter(url, paramString);
}
let promise = new Promise((resolve, reject) => {
OData.read({
requestUri: url,
headers: core.extend({}, this.headers)
}, function (data, response) {
let inlineCount;
if (data.__count) {
// OData can return data.__count as a string
inlineCount = parseInt(data.__count, 10);
}
// Odata returns different result structure when it returns multiple entities (data.results) vs single entity (data directly).
// @see http://www.odata.org/documentation/odata-version-2-0/json-format/#RepresentingCollectionsOfEntries
// and http://www.odata.org/documentation/odata-version-2-0/json-format/#RepresentingEntries
let results;
if (data.results) {
results = data.results;
}
else {
results = data;
}
return resolve({ results: results, inlineCount: inlineCount, httpResponse: response, query: query });
}, function (error) {
return reject(createError(error, url));
});
});
return promise;
}
saveChanges(odataSaveContext, saveBundle) {
let adapter = odataSaveContext.adapter = this;
let saveContext = odataSaveContext;
let url;
if (this.relativeUrl === true) {
saveContext.routePrefix = adapter.getRoutePrefix(saveContext.dataService);
url = saveContext.dataService.qualifyUrl("$batch");
}
else if (core.isFunction(adapter.relativeUrl)) {
saveContext.routePrefix = adapter.relativeUrl(saveContext.dataService, '');
url = saveContext.routePrefix + '$batch';
}
else {
saveContext.routePrefix = adapter.getAbsoluteUrl(saveContext.dataService, '');
url = saveContext.routePrefix + '$batch';
}
let requestData = createChangeRequests(saveContext, saveBundle);
let tempKeys = saveContext.tempKeys;
let contentKeys = saveContext.contentKeys;
let promise = new Promise((resolve, reject) => {
OData.request({
headers: core.extend({}, this.headers),
requestUri: url,
method: "POST",
data: requestData
}, function (data, response) {
let entities = [];
let keyMappings = [];
let saveResult = { entities: entities, keyMappings: keyMappings };
data.__batchResponses.forEach(function (br) {
br.__changeResponses.forEach(function (cr) {
let response = cr.response || cr;
let statusCode = response.statusCode;
if ((!statusCode) || statusCode >= 400) {
reject(createError(cr, url));
return;
}
let contentId = cr.headers["Content-ID"];
// Olingo sends different case of 'ID' for the header name.
if (!contentId) {
contentId = cr.headers["Content-Id"];
}
let rawEntity = cr.data;
if (rawEntity) {
let tempKey = tempKeys[contentId];
if (tempKey) {
let entityType = tempKey.entityType;
if (entityType.autoGeneratedKeyType !== breeze.AutoGeneratedKeyType.None) {
let tempValue = tempKey.values[0];
let realKey = entityType.getEntityKeyFromRawEntity(rawEntity, breeze.DataProperty.getRawValueFromServer);
let keyMapping = { entityTypeName: entityType.name, tempValue: tempValue, realValue: realKey.values[0] };
keyMappings.push(keyMapping);
}
}
entities.push(rawEntity);
}
else {
let origEntity = contentKeys[contentId];
entities.push(origEntity);
}
});
});
return resolve(saveResult);
}, function (err) {
return reject(createError(err, url));
}, OData.batchHandler);
});
return promise;
}
jsonResultsAdapter = new breeze.JsonResultsAdapter({
name: "OData_default",
visitNode: function (node, mappingContext, nodeContext) {
let result = {};
if (node == null)
return result;
let metadata = node.__metadata;
if (metadata != null) {
// TODO: may be able to make this more efficient by caching of the previous value.
let entityTypeName = breeze.MetadataStore.normalizeTypeName(metadata.type);
let et = entityTypeName && mappingContext.entityManager.metadataStore.getEntityType(entityTypeName, true);
// OData response doesn't distinguish a projection from a whole entity.
// We'll assume that whole-entity data would have at least as many properties (<=)
// as the EntityType has mapped properties on the basis that
// most projections remove properties rather than add them.
// If not, assume it's a projection and do NOT treat as an entity
if (et && et._mappedPropertiesCount <= Object.keys(node).length - 1) {
// if (et && et._mappedPropertiesCount === Object.keys(node).length - 1) { // OLD
result.entityType = et;
let uriKey = metadata.uri || metadata.id;
if (uriKey) {
// Strip baseUri to make uriKey a relative uri
// Todo: why is this necessary when absolute works for every OData source tested?
let re = new RegExp('^' + mappingContext.dataService.serviceName, 'i');
uriKey = uriKey.replace(re, '');
}
result.extraMetadata = {
uriKey: uriKey,
etag: metadata.etag
};
}
}
// OData v3 - projection arrays will be enclosed in a results array
if (node.results) {
result.node = node.results;
}
let propertyName = nodeContext.propertyName;
result.ignore = node.__deferred != null || propertyName === "__metadata" ||
// EntityKey properties can be produced by EDMX models
(propertyName === "EntityKey" && node.$type && core.stringStartsWith(node.$type, "System.Data"));
return result;
}
});
getAbsoluteUrl(dataService, url) {
let serviceName = dataService.qualifyUrl('');
// only prefix with serviceName if not already on the url
let base = (core.stringStartsWith(url, serviceName)) ? '' : serviceName;
// If no protocol, turn base into an absolute URI
if (window && serviceName.indexOf('//') < 0) {
// no protocol; make it absolute
base = window.location.protocol + '//' + window.location.host +
(core.stringStartsWith(serviceName, '/') ? '' : '/') +
base;
}
return base + url;
}
getRoutePrefix(dataService) {
// Get the routePrefix from a Web API OData service name.
// The routePrefix is presumed to be the pathname within the dataService.serviceName
// Examples of servicename -> routePrefix:
// 'http://localhost:55802/odata/' -> 'odata/'
// 'http://198.154.121.75/service/odata/' -> 'service/odata/'
let parser;
if (typeof document === 'object') { // browser
parser = document.createElement('a');
parser.href = dataService.serviceName;
}
else { // node
// TODO: how to best handle this
// assumes existence of node's url.parse method.
parser = url.parse(dataService.serviceName);
}
let prefix = parser.pathname;
if (prefix[0] === '/') {
prefix = prefix.substr(1);
} // drop leading '/' (all but IE)
if (prefix.substr(-1) !== '/') {
prefix += '/';
} // ensure trailing '/'
return prefix;
}
}
// crude serializer. Doesn't recurse
function toQueryString(obj) {
let parts = [];
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i]));
}
}
return parts.join("&");
}
/** Transform data properties while unwrapping entities */
function transformValue(prop, val) {
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.
val = val && new Date(val.getTime() - (val.getTimezoneOffset() * 60000));
}
else if (prop.dataType === breeze.DataType.DateOnly) {
val = breeze.DataType.toDateOnlyString(val);
}
else if (prop.dataType.quoteJsonOData) {
val = val != null ? val.toString() : val;
}
return val;
}
function createChangeRequests(saveContext, saveBundle) {
let changeRequestInterceptor = saveContext.adapter._createChangeRequestInterceptor(saveContext, saveBundle);
let changeRequests = [];
let tempKeys = [];
let contentKeys = [];
let entityManager = saveContext.entityManager;
let helper = entityManager.helper;
let id = 0;
let routePrefix = saveContext.routePrefix;
saveBundle.entities.forEach(function (entity, index) {
let aspect = entity.entityAspect;
id = id + 1; // we are deliberately skipping id=0 because Content-ID = 0 seems to be ignored.
let request = { headers: { "Content-ID": id, "DataServiceVersion": "2.0" } };
contentKeys[id] = entity;
if (aspect.entityState.isAdded()) {
request.requestUri = routePrefix + entity.entityType.defaultResourceName;
request.method = "POST";
request.data = helper.unwrapInstance(entity, transformValue);
tempKeys[id] = aspect.getKey();
}
else if (aspect.entityState.isModified()) {
updateDeleteMergeRequest(request, aspect, routePrefix);
request.method = "MERGE";
request.data = helper.unwrapChangedValues(entity, entityManager.metadataStore, transformValue);
// should be a PATCH/MERGE
}
else if (aspect.entityState.isDeleted()) {
updateDeleteMergeRequest(request, aspect, routePrefix);
request.method = "DELETE";
}
else {
return;
}
request = changeRequestInterceptor.getRequest(request, entity, index);
changeRequests.push(request);
});
saveContext.contentKeys = contentKeys;
saveContext.tempKeys = tempKeys;
changeRequestInterceptor.done(changeRequests);
return {
__batchRequests: [
{
__changeRequests: changeRequests
}
]
};
}
function updateDeleteMergeRequest(request, aspect, routePrefix) {
let uriKey;
let extraMetadata = aspect.extraMetadata;
if (extraMetadata == null) {
uriKey = getUriKey(aspect);
aspect.extraMetadata = {
uriKey: uriKey
};
}
else {
uriKey = extraMetadata.uriKey;
if (extraMetadata.etag) {
request.headers["If-Match"] = extraMetadata.etag;
}
}
request.requestUri =
// use routePrefix if uriKey lacks protocol (i.e., relative uri)
uriKey.indexOf('//') > 0 ? uriKey : routePrefix + uriKey;
}
function getUriKey(aspect) {
let entityType = aspect.entity.entityType;
let resourceName = entityType.defaultResourceName;
let kps = entityType.keyProperties;
let uriKey = resourceName + "(";
if (kps.length === 1) {
uriKey = uriKey + fmtProperty(kps[0], aspect) + ")";
}
else {
let delim = "";
kps.forEach(function (kp) {
uriKey = uriKey + delim + kp.nameOnServer + "=" + fmtProperty(kp, aspect);
delim = ",";
});
uriKey = uriKey + ")";
}
return uriKey;
}
function fmtProperty(prop, aspect) {
return prop.dataType.fmtOData(aspect.getPropertyValue(prop.name));
}
function createError(error, url) {
// OData errors can have the message buried very deeply - and nonobviously
// this code is tricky so be careful changing the response.body parsing.
let result = new Error();
let response = error && error.response;
if (!response) {
// in case DataJS returns "No handler for this data"
result.message = error;
result.statusText = error;
return result;
}
result.message = response.statusText;
result.statusText = response.statusText;
result.status = response.statusCode;
// non std
if (url)
result.url = url;
result.body = response.body;
if (response.body) {
let nextErr;
try {
let body = JSON.parse(response.body);
result.body = body;
// OData v3 logic
if (body['odata.error']) {
body = body['odata.error'];
}
let msg = "";
do {
nextErr = body.error || body.innererror;
if (!nextErr)
msg = msg + getMessage(body);
nextErr = nextErr || body.internalexception;
body = nextErr || body;
} while (nextErr);
if (msg.length > 0) {
result.message = msg;
}
}
catch (e) {
}
}
breeze.AbstractDataServiceAdapter._catchNoConnectionError(result);
return result;
}
function getMessage(body) {
let msg = body.message || "";
return ((typeof (msg) === "string") ? msg : msg.value) + "; ";
}
breeze.config.registerAdapter("dataService", DataServiceODataAdapter);
let webApiODataCtor = function () {
this.name = "webApiOData";
};
breeze.core.extend(webApiODataCtor.prototype, DataServiceODataAdapter.prototype);
breeze.config.registerAdapter("dataService", webApiODataCtor);
// OData 4 adapter
let webApiOData4Ctor = function () {
this.name = "webApiOData4";
};
breeze.core.extend(webApiOData4Ctor.prototype, webApiODataCtor.prototype);
webApiOData4Ctor.prototype.initialize = function () {
// Aargh... they moved the cheese.
let datajs = core.requireLib("datajs", "Needed to support remote OData v4 services");
OData = datajs.V4.oData;
OData.json.jsonHandler.recognizeDates = true;
};
webApiOData4Ctor.prototype.headers = { "OData-Version": "4.0" };
breeze.config.registerAdapter("dataService", webApiOData4Ctor);
/**
* Generated bundle index. Do not edit.
*/
export { DataServiceODataAdapter };
//# sourceMappingURL=breeze-client-adapter-data-service-odata.mjs.map