UNPKG

@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
'use strict'; 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]); } }