@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
566 lines (497 loc) • 17.1 kB
JavaScript
;
const XMLWriter = require('xml-writer');
const InternalError = require('./../utils/errors/internalError');
const typeConverter = require('./../utils/typeConverter');
const dbSegment = require('./../db/dbSegment');
const Http404_NotFound = require('./../utils/errors/http/notFound');
const associationsUtil = require('./../utils/associations');
const NotImplemented = require('./../utils/errors/http/notImplemented');
module.exports = AtomSerializer;
function AtomSerializer(context) {
if (!context) {
throw new InternalError('Context must be specified.');
}
this._context = context;
this._xmlWriter = new XMLWriter();
}
function startDocument(xmlWriter) {
xmlWriter.startDocument('1.0', 'utf-8', true);
}
/**
* Serializes OData feed represented as the dbSegment input parameter.
* @param dbSegment - DB segment, which represents the OData feed, which should be serialized.
* @param inlineCount - inlineCount value for the feed. This parameter must be provided when $inlinecount query option
* has been specified with value "allpages".
* @returns XML string, which represents the Atom Feed for the specified dbSegment.
*/
AtomSerializer.prototype.serializeFeed = function (dbSegment, inlineCount) {
startDocument(this._xmlWriter);
const feedInfo = createRootFeedInfo(this._context, dbSegment);
_serializeFeed(
this._context,
this._xmlWriter,
feedInfo,
dbSegment,
inlineCount
);
this._xmlWriter.endDocument();
return this._xmlWriter.toString();
};
/**
* Serializes OData entry represented as the dbSegment input parameter.
* @param dbSegment - DB segment, which represents the OData entry, which should be serialized.
* @returns XML string, which represents the Atom Entry for the specified dbSegment.
*/
AtomSerializer.prototype.serializeEntity = function (dbSegment) {
startDocument(this._xmlWriter);
const entity = getEntityFromSegment(dbSegment);
const converters = createConverters(dbSegment);
_serializeEntry(
this._context,
this._xmlWriter,
dbSegment,
entity,
converters,
createNamespaceAttributes(this._context)
);
this._xmlWriter.endDocument();
return this._xmlWriter.toString();
};
/**
* Serializes OData property represented as the dbSegment input parameter.
*/
AtomSerializer.prototype.serializeProperty = function () {
throw new NotImplemented(
'Atom format is not supported for property serialization.',
this._context
);
};
function _serializeFeed(context, xmlWriter, feedInfo, segment, inlineCount) {
xmlWriter.startElement('feed');
if (feedInfo.attributes) {
addAttributes(xmlWriter, feedInfo.attributes);
}
createFeedHeader(context, xmlWriter, feedInfo);
addInlineCount(xmlWriter, inlineCount);
const converters = createConverters(segment);
for (const feedInfoEntry of feedInfo.entries) {
_serializeEntry(context, xmlWriter, segment, feedInfoEntry, converters);
}
xmlWriter.endElement();
}
function addInlineCount(xmlWriter, inlineCount) {
if (inlineCount) {
xmlWriter.writeElement('m:count', inlineCount);
}
}
/**
* Creates top XML elements for the OData feed element.
* @param context - OData context.
* @param xmlWriter - XML writer, which should be used during the serialization.
* @param feedInfo - object, which encapsulates the required info about the OData feed.
*/
function createFeedHeader(context, xmlWriter, feedInfo) {
createTitleElement(xmlWriter, feedInfo.title);
xmlWriter.writeElement('id', context.uriTree.baseUrl + feedInfo.href);
// $SAPINFO 'updated' element is not added to the output for backwards compatibility with XS1
// xmlWriter.writeElement('updated', new Date().toString());
createAuthorElement(xmlWriter);
createLink(xmlWriter, 'self', feedInfo.title, feedInfo.href);
}
function createLink(xmlWriter, relation, title, href) {
xmlWriter.startElement('link');
xmlWriter.writeAttribute('rel', relation);
xmlWriter.writeAttribute('title', title);
xmlWriter.writeAttribute('href', href);
xmlWriter.endElement();
}
function createTitleElement(xmlWriter, title) {
title = title || '';
xmlWriter.startElement('title');
xmlWriter.writeAttribute('type', 'text');
xmlWriter.text(title);
xmlWriter.endElement();
}
function createAuthorElement(xmlWriter) {
xmlWriter.startElement('author');
xmlWriter.startElement('name').endElement();
xmlWriter.endElement();
}
function _serializeEntry(
context,
xmlWriter,
segment,
entity,
converters,
attributes
) {
xmlWriter.startElement('entry');
if (attributes) {
addAttributes(xmlWriter, attributes);
}
createEntryHeader(context, xmlWriter, segment, entity);
serializeNavigationProperties(context, xmlWriter, segment, entity);
serializePrimitiveProperties(
context,
xmlWriter,
segment,
entity,
converters
);
xmlWriter.endElement();
}
/**
* Retrieves OData entity from the specified dbSegment.
* @param dbSegment
* @returns JSON object containing the entity data or undefined, if the specified dbSegment does not contain the
* required entity.
*/
function getEntityFromSegment(dbSegment) {
const entities = dbSegment.getRowsWithGenKey();
if (!entities || !entities.length || entities.length === 0) {
throw new Http404_NotFound('Entity not found.');
}
return entities[0];
}
function serializeNavigationProperties(context, xmlWriter, segment, entity) {
for (const selectedNavigation of segment._SelectedNavigations) {
const navigationSegment =
segment.getRelevantNavigationSegments()[selectedNavigation];
serializeNavigationProperty(
context,
xmlWriter,
segment,
navigationSegment,
entity,
selectedNavigation
);
}
}
/**
* Returns true if the navigation property with the specified navPropertyName and belonging to the specified
* navigationSegment is a collection (i.e. target EDM multiplicity is *). The method returns false otherwise.
*/
function isNavigationCollection(
context,
parentSegment,
navPropertyName,
navigationSegment
) {
if (navigationSegment) {
return navigationSegment.isCollection;
}
const navigation = parentSegment.entityType.getNavigation(navPropertyName);
const association = context.gModel.getAssociation(navigation.association);
const targetEnd = associationsUtil.getTargetEnd(
navigation,
association,
parentSegment.entityType
);
return targetEnd.multiplicity === '*';
}
function serializeNavigationProperty(
context,
xmlWriter,
parentSegment,
navigationSegment,
parentEntity,
navPropertyName
) {
const isCollection = isNavigationCollection(
context,
parentSegment,
navPropertyName,
navigationSegment
);
startNavigationLink(
xmlWriter,
parentSegment,
navPropertyName,
isCollection,
parentEntity
);
if (
navigationSegment &&
navigationSegment.isExpand &&
navigationSegment.kind === dbSegment.DBS_Navigation
) {
xmlWriter.startElement('m:inline');
if (isCollection) {
serializeNavigationCollection(
context,
xmlWriter,
navPropertyName,
navigationSegment,
parentSegment,
parentEntity
);
} else {
serializeNavigationEntity(
context,
xmlWriter,
navigationSegment,
parentEntity['1row']
);
}
xmlWriter.endElement();
}
xmlWriter.endElement();
}
/**
* Creates 'info' object, which is required for Atom feed serialization for the specified navigation property.
* @param navPropertyName - name of the navigation property.
* @param navigationSegment - DB segment, which represents the navigation property.
* @param parentSegment - DB segment, which is a parent segment for the DB segment, which represents the navigation
* property.
* @param parentEntity - OData entity, which contains the navigation property.
* @returns JSON object containing all the required info for the serialization of the multi-valued navigation property.
*/
function createFeedInfoForNavProperty(
navPropertyName,
navigationSegment,
parentSegment,
parentEntity
) {
const navigationEntities = getNavigationEntities(
navigationSegment,
parentEntity['1row']
);
const parentEntityHref = createEntityRelativeUrl(
parentSegment,
parentEntity
);
const feedHref = parentEntityHref + '/' + navPropertyName;
return createFeedInfo(navPropertyName, feedHref, navigationEntities);
}
function serializeNavigationCollection(
context,
xmlWriter,
navPropertyName,
navigationSegment,
parentSegment,
parentEntity
) {
const feedInfo = createFeedInfoForNavProperty(
navPropertyName,
navigationSegment,
parentSegment,
parentEntity
);
if (feedInfo.entries.length === 0) {
return;
}
_serializeFeed(context, xmlWriter, feedInfo, navigationSegment);
}
function startNavigationLink(
xmlWriter,
parentSegment,
navPropertyName,
isNavigationCollection,
parentEntity
) {
const type = isNavigationCollection ? 'feed' : 'entry';
xmlWriter.startElement('link');
xmlWriter.writeAttribute(
'rel',
'http://schemas.microsoft.com/ado/2007/08/dataservices/related/' +
navPropertyName
);
xmlWriter.writeAttribute('type', 'application/atom+xml;type=' + type);
xmlWriter.writeAttribute('title', navPropertyName);
const parentEntityUrl = createEntityRelativeUrl(
parentSegment,
parentEntity
);
xmlWriter.writeAttribute('href', parentEntityUrl + '/' + navPropertyName);
}
/**
* Retrieves OData entities for the navigation property, which is represented by the specified navigationSegment.
* @param navigationSegment - DB segment, which represents the navigation property.
* @param parentId - ID of the parent entity. i.e. of the entity, which contains the navigation property.
*/
function getNavigationEntities(navigationSegment, parentId) {
const entities = navigationSegment.getRowsWithGenKey();
const navigationEntities = [];
let navigationEntity = entities[navigationSegment.sql.readPosition];
while (navigationEntity && navigationEntity['0row'] === parentId) {
navigationEntities.push(navigationEntity);
navigationSegment.sql.readPosition++;
navigationEntity = entities[navigationSegment.sql.readPosition];
}
return navigationEntities;
}
function serializeNavigationEntity(
context,
xmlWriter,
navigationSegment,
parentId
) {
const entity = getNavigationEntity(navigationSegment, parentId);
if (!entity) {
return;
}
const converters = createConverters(navigationSegment);
_serializeEntry(context, xmlWriter, navigationSegment, entity, converters);
}
/**
* Retrieves OData entity for the navigation property, which is represented by the specified navigationSegment.
* @param navigationSegment - DB segment for the navigation property.
* @param parentId - ID of the parent OData entity.
*/
function getNavigationEntity(navigationSegment, parentId) {
const entities = navigationSegment.getRowsWithGenKey();
for (const entity of entities) {
if (entity['0row'] === parentId) {
return entity;
}
}
}
function serializePrimitiveProperties(
context,
xmlWriter,
segment,
entity,
converters
) {
writeCategory(context, xmlWriter, segment);
xmlWriter.startElement('content');
xmlWriter.writeAttribute('type', 'application/xml');
xmlWriter.startElement('m:properties');
const selectedProperties = segment.getSelectedPropsWithGenKey();
for (let i = 0; i < selectedProperties.length; i++) {
const propertyName = selectedProperties[i];
const propertyValue = converters[i](entity[propertyName]);
const propertyType = getEdmPropertyType(segment, propertyName);
serializeEntityProperty(
xmlWriter,
propertyName,
propertyType,
propertyValue
);
}
xmlWriter.endElement();
xmlWriter.endElement();
}
function writeCategory(context, xmlWriter, segment) {
xmlWriter.startElement('category');
const term =
context.gModel.getNamespace() + '.' + segment.entityType.name + 'Type';
xmlWriter.writeAttribute('term', term);
xmlWriter.writeAttribute(
'scheme',
'http://schemas.microsoft.com/ado/2007/08/dataservices/scheme'
);
xmlWriter.endElement();
}
function getEdmPropertyType(segment, propertyName) {
const dbType = getPropertyDBType(segment, propertyName);
return typeConverter.dbTypeNameToODataTypeName[dbType];
}
function getPropertyDBType(segment, propertyName) {
const entityType = segment.entityType;
const property = entityType.propertiesMap[propertyName];
if (property) {
return property.DATA_TYPE_NAME;
}
// if there is no property with the required name in the propertiesMap, then check whether the required property
// represents a generated key.
if (entityType.keys.generatedKey === propertyName) {
// VARCHAR is always used as the type for the generated keys.
return 'VARCHAR';
}
throw new Error(propertyName + ' is an invalid entity type property.');
}
function serializeEntityProperty(
xmlWriter,
propertyName,
propertyType,
propertyValue
) {
propertyName = 'd:' + propertyName;
xmlWriter.startElement(propertyName);
xmlWriter.writeAttribute('m:type', propertyType);
/* jshint eqnull:true */
if (propertyValue == null) {
xmlWriter.writeAttribute('m:null', 'true');
} else {
xmlWriter.text(propertyValue.toString());
}
xmlWriter.endElement();
}
/**
* Creates top level XML elements for the Atom entry element.
*/
function createEntryHeader(context, xmlWriter, segment, entry) {
const entityUrl = createEntityAbsoluteUrl(context, segment, entry);
xmlWriter.writeElement('id', entityUrl);
createTitleElement(xmlWriter);
createAuthorElement(xmlWriter);
createEntityEditLink(xmlWriter, segment, entry);
}
function createEntityEditLink(xmlWriter, segment, entity) {
const entityRelativeUrl = createEntityRelativeUrl(segment, entity);
createLink(xmlWriter, 'edit', segment.entityType.name, entityRelativeUrl);
}
function createEntityAbsoluteUrl(context, segment, entity) {
return context.uriTree.baseUrl + createEntityRelativeUrl(segment, entity);
}
function createEntityRelativeUrl(segment, entity) {
let relativeUrl = segment.entityType.name;
const keysProperties = segment.getKeysProperties();
relativeUrl += '(';
relativeUrl += keysProperties.map(toValues).join(',');
return relativeUrl + ')';
function toValues(keyProperty, index, array) {
const value = typeConverter.serializeDbValueToUriLiteral(
entity[index.toString()] /*keys[index]*/,
keyProperty
);
if (array.length === 1) {
return value;
}
return keyProperty.COLUMN_NAME + '=' + value;
}
}
function createConverters(segment) {
return segment
.getSelectedPropsWithGenKey()
.map(function toConverter(property) {
return segment.entityType.converterMapToXMLPayload[property];
});
}
function createRootFeedInfo(context, segment) {
const feedTitle = getLastUriSegment(context);
const feedInfo = createFeedInfo(
feedTitle,
feedTitle,
segment.getRowsWithGenKey()
);
feedInfo.attributes = createNamespaceAttributes(context);
return feedInfo;
}
function getLastUriSegment(context) {
const uriSegments = context.uriTree.segments.decoded;
return uriSegments[uriSegments.length - 1];
}
function createFeedInfo(title, href, entries) {
const feedInfo = {};
feedInfo.title = title;
feedInfo.href = href;
feedInfo.entries = entries;
return feedInfo;
}
function createNamespaceAttributes(context) {
return {
'xml:base': context.uriTree.baseUrl,
'xmlns:d': 'http://schemas.microsoft.com/ado/2007/08/dataservices',
'xmlns:m':
'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata',
'xmlns': 'http://www.w3.org/2005/Atom',
};
}
function addAttributes(xmlWriter, attributes) {
/*jshint forin: false */
for (const attributeName in attributes) {
xmlWriter.writeAttribute(attributeName, attributes[attributeName]);
}
}