UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

416 lines (347 loc) 15.9 kB
'use strict'; var XMLWriter = require('xml-writer'); var InternalError = require('./../utils/errors/internalError'); var typeConverter = require('./../utils/typeConverter'); var dbSegment = require('./../db/dbSegment'); var Http404_NotFound = require('./../utils/errors/http/notFound'); var associationsUtil = require('./../utils/associations'); var 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); var 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); var entity = getEntityFromSegment(dbSegment); var 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); var converters = createConverters(segment); for (var i = 0; i < feedInfo.entries.length; i++) { _serializeEntry(context, xmlWriter, segment, feedInfo.entries[i], 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) { var 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 (var i = 0; i < segment._SelectedNavigations.length; i++) { var selectedNavigation = segment._SelectedNavigations[i]; var 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; } var navigation = parentSegment.entityType.getNavigation(navPropertyName); var association = context.gModel.getAssociation(navigation.association); var targetEnd = associationsUtil.getTargetEnd(navigation, association, parentSegment.entityType); return targetEnd.multiplicity === "*"; } function serializeNavigationProperty(context, xmlWriter, parentSegment, navigationSegment, parentEntity, navPropertyName) { var 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) { var navigationEntities = getNavigationEntities(navigationSegment, parentEntity['1row']); var parentEntityHref = createEntityRelativeUrl(parentSegment, parentEntity); var feedHref = parentEntityHref + "/" + navPropertyName; return createFeedInfo(navPropertyName, feedHref, navigationEntities); } function serializeNavigationCollection(context, xmlWriter, navPropertyName, navigationSegment, parentSegment, parentEntity) { var feedInfo = createFeedInfoForNavProperty(navPropertyName, navigationSegment, parentSegment, parentEntity); if(feedInfo.entries.length === 0) { return; } _serializeFeed(context, xmlWriter, feedInfo, navigationSegment); } function startNavigationLink(xmlWriter, parentSegment, navPropertyName, isNavigationCollection, parentEntity) { var 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); var 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) { var entities = navigationSegment.getRowsWithGenKey(); var navigationEntities = []; var navigationEntity; while((navigationEntity = entities[navigationSegment.sql.readPosition]) && (navigationEntity['0row'] === parentId)) { navigationEntities.push(navigationEntity); navigationSegment.sql.readPosition++; } return navigationEntities; } function serializeNavigationEntity(context, xmlWriter, navigationSegment, parentId) { var entity = getNavigationEntity(navigationSegment, parentId); if(!entity) { return; } var 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) { var entities = navigationSegment.getRowsWithGenKey(); for(var i = 0; i < entities.length; i++) { var entity = entities[i]; 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"); var selectedProperties = segment.getSelectedPropsWithGenKey(); for(var i = 0; i < selectedProperties.length; i++) { var propertyName = selectedProperties[i]; var propertyValue = converters[i](entity[propertyName]); var propertyType = getEdmPropertyType(segment, propertyName); serializeEntityProperty(xmlWriter, propertyName, propertyType, propertyValue); } xmlWriter.endElement(); xmlWriter.endElement(); } function writeCategory(context, xmlWriter, segment) { xmlWriter.startElement("category"); var 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) { var dbType = getPropertyDBType(segment, propertyName); return typeConverter.dbTypeNameToODataTypeName[dbType]; } function getPropertyDBType(segment, propertyName) { var entityType = segment.entityType; var 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) { var entityUrl = createEntityAbsoluteUrl(context, segment, entry); xmlWriter.writeElement("id", entityUrl); createTitleElement(xmlWriter); createAuthorElement(xmlWriter); createEntityEditLink(xmlWriter, segment, entry); } function createEntityEditLink(xmlWriter, segment, entity) { var 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) { var relativeUrl = segment.entityType.name; var keysProperties = segment.getKeysProperties(); relativeUrl += '('; relativeUrl += keysProperties.map(toValues).join(','); return relativeUrl + ')'; function toValues(keyProperty, index, array) { var 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) { var feedTitle = getLastUriSegment(context); var feedInfo = createFeedInfo(feedTitle, feedTitle, segment.getRowsWithGenKey()); feedInfo.attributes = createNamespaceAttributes(context); return feedInfo; } function getLastUriSegment(context) { var uriSegments = context.uriTree.segments.decoded; return uriSegments[uriSegments.length - 1]; } function createFeedInfo(title, href, entries) { var 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(var attributeName in attributes) { xmlWriter.writeAttribute(attributeName, attributes[attributeName]); } }