UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,448 lines (1,269 loc) 54.8 kB
'use strict'; const edmUtils = require('./edmUtils'); const { isBuiltinType } = require('../base/builtins'); const { forEach } = require('../utils/objectUtils'); const { EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, } = require('./EdmPrimitiveTypeDefinitions.js'); const { CompilerAssertion } = require('../base/error'); function getEdm( options ) { class Node { /** * @param {boolean[]} version Versions in the form of [<v2>, <v4>]. * @param {object} attributes * @param {CSN.Model} csn */ constructor(version, attributes = Object.create(null), csn = undefined) { if (!attributes || typeof attributes !== 'object') throw new CompilerAssertion('Debug me: attributes must be a dictionary'); if (!Array.isArray(version)) throw new CompilerAssertion(`Debug me: v is either undefined or not an array: ${ version }`); if (version.filter(v => v).length !== 1) throw new CompilerAssertion('Debug me: exactly one version must be set'); // Common attributes of JSON and XML. // Note: Can't assign attributes directly, due to the input object being modified. // The caller re-uses the object for other nodes. this._edmAttributes = Object.assign(Object.create(null), attributes); this._xmlOnlyAttributes = Object.create(null); this._jsonOnlyAttributes = Object.create(null); this._openApiHints = Object.create(null); this._children = []; this._ignoreChildren = false; this._v = version; if (this.v2) this.setSapVocabularyAsAttributes(csn); this.setOpenApiHints(csn); } get v2() { return this._v[0]; } get v4() { return this._v[1]; } get kind() { return this.constructor.name; } /** * Set the EDM(X) attribute on the Node. * @param {string} key * @param {any} value */ setEdmAttribute(key, value) { if (key !== undefined && key !== null && value !== undefined && value !== null) this._edmAttributes[key] = value; } /** * Remove the EDM(X) attribute on the Node. * @param {string} name */ removeEdmAttribute(name) { if (name in this._edmAttributes) delete this._edmAttributes[name]; if (name in this._xmlOnlyAttributes) delete this._xmlOnlyAttributes[name]; } /** * Set properties that should only appear in the XML representation * @param {object} attributes * @return {any} */ setXml(attributes) { return Object.assign(this._xmlOnlyAttributes, attributes); } /** * Set properties that should only appear in the JSON representation. * Today JSON attributes are not rendered in toJSONattributes() * * @param {object} attributes * @return {any} */ setJSON(attributes) { return Object.assign(this._jsonOnlyAttributes, attributes); } prepend(...children) { this._children.splice(0, 0, ...children.filter(c => c)); return this; } append(...children) { // remove undefined entries this._children.push(...children.filter(c => c)); return this; } setOpenApiHints(csn) { if (csn && options.odataOpenapiHints) { const jsonAttr = Object.create(null); Object.entries(csn).filter(([ k, _v ] ) => k.startsWith('@OpenAPI.')).forEach(([ k, v ]) => { jsonAttr[k] = v; }); Object.assign(this._openApiHints, jsonAttr); } return this._openApiHints; } // virtual toJSON() { const json = Object.create(null); // $kind Property MAY be omitted in JSON for performance reasons if (!(this.kind in Node.noJsonKinds)) json.$Kind = this.kind; return this.toJSONchildren(this.toJSONattributes(json)); } // virtual toJSONattributes(json, withHints = true) { forEach(this._edmAttributes, (p, v) => { if (p !== 'Name') json[p[0] === '@' ? p : `$${ p }`] = v; }); return (withHints ? this.toOpenApiHints(json) : json); } toOpenApiHints(json) { if (options.odataOpenapiHints && this._openApiHints) { Object.entries(this._openApiHints).forEach(([ p, v ]) => { json[p[0] === '@' ? p : `$${ p }`] = v; }); } return json; } // virtual toJSONchildren(json) { // any child with a Name should be added by its name into the JSON object // all others must overload toJSONchildren() this._children.filter(c => c._edmAttributes.Name).forEach((c) => { json[c._edmAttributes.Name] = c.toJSON(); }); return json; } // virtual toXML(indent = '', what = 'all') { const { kind } = this; let head = `${ indent }<${ kind }`; if (kind === 'Parameter' && this._edmAttributes.Collection) { delete this._edmAttributes.Collection; this._edmAttributes.Type = `Collection(${ this._edmAttributes.Type })`; } head += this.toXMLattributes(); const inner = this.innerXML(`${ indent } `, what); if (inner.length < 1) head += '/>'; else if (inner.length < 77 && inner.indexOf('<') < 0) head += `>${ inner.slice(indent.length + 1, -1) }</${ kind }>`; else head += `>\n${ inner }${ indent }</${ kind }>`; return head; } // virtual toXMLattributes() { let tmpStr = ''; forEach(this._edmAttributes, (p, v) => { if (v !== undefined && typeof v !== 'object') tmpStr += ` ${ p }="${ edmUtils.escapeStringForAttributeValue(v) }"`; }); forEach(this._xmlOnlyAttributes, (p, v) => { if (v !== undefined && typeof v !== 'object') tmpStr += ` ${ p }="${ edmUtils.escapeStringForAttributeValue(v) }"`; }); return tmpStr; } // virtual innerXML(indent, what = 'all') { let xml = ''; this._children.forEach((e) => { xml += `${ e.toXML(indent, what) }\n`; }); return xml; } // virtual setSapVocabularyAsAttributes(csn, useSetAttributes = false) { if (csn) { const attr = (useSetAttributes ? csn._SetAttributes : csn); if (attr) { Object.entries(attr).forEach(([ p, v ]) => { if (p.match(/^@sap./)) this.setXml( { [`sap:${ p.slice(5).replace(/\./g, '-') }`]: v } ); }); } } } } // $kind Property MAY be omitted in JSON for performance reasons Node.noJsonKinds = { Property: 1, EntitySet: 1, ActionImport: 1, FunctionImport: 1, Singleton: 1, Schema: 1, }; class Reference extends Node { constructor(version, details) { super(version, details); if (this.v2) this._edmAttributes['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx'; } get kind() { return 'edmx:Reference'; } toJSON() { const json = Object.create(null); const includes = []; this._children.forEach(c => includes.push(c.toJSON())); if (includes.length > 0) json.$Include = includes; return json; } } class Include extends Node { get kind() { return 'edmx:Include'; } toJSON() { const json = Object.create(null); return this.toJSONattributes(json); } } class EntityContainer extends Node { constructor(version, attributes, csn) { super(version, attributes, csn); this._registry = Object.create(null); } // use the _SetAttributes setSapVocabularyAsAttributes(csn) { super.setSapVocabularyAsAttributes(csn, true); } toJSONattributes(json) { return super.toJSONattributes(json, false); } register(entry) { if (!this._registry[entry._edmAttributes.Name]) this._registry[entry._edmAttributes.Name] = [ entry ]; else this._registry[entry._edmAttributes.Name].push(entry); this.append(entry); } } class Schema extends Node { constructor(version, ns, alias = undefined, serviceCsn = null, annotations = [], withEntityContainer = true) { const props = { Namespace: ns }; if (alias !== undefined) props.Alias = alias; super(version, props); this.setOpenApiHints(serviceCsn); this._annotations = annotations; this._actions = Object.create(null); this.setXml( { xmlns: (this.v2) ? 'http://schemas.microsoft.com/ado/2008/09/edm' : 'http://docs.oasis-open.org/odata/ns/edm' } ); if (this.v2 && serviceCsn) this.setSapVocabularyAsAttributes(serviceCsn); if (withEntityContainer) { const ecprops = { Name: 'EntityContainer' }; const ec = new EntityContainer(version, ecprops, serviceCsn ); if (this.v2) ec.setXml( { 'm:IsDefaultEntityContainer': true } ); // append for rendering, ok ec has Name this.append(ec); // set as attribute for later access... this._ec = ec; } } // hold actions and functions in V4 addAction(action) { if (this._actions[action._edmAttributes.Name]) this._actions[action._edmAttributes.Name].push(action); else this._actions[action._edmAttributes.Name] = [ action ]; } setAnnotations(annotations) { if (Array.isArray(annotations) && annotations.length > 0) this._annotations.push(...annotations); } innerXML(indent, what) { let xml = ''; if (what === 'metadata' || what === 'all') { xml += super.innerXML(indent); if (this._actions) { Object.values(this._actions).forEach((actionArray) => { actionArray.forEach((action) => { xml += `${ action.toXML(indent, what) }\n`; }); }); } } if ((what === 'annotations' || what === 'all') && this._annotations.length > 0) { this._annotations.filter(a => a._edmAttributes.Term).forEach((a) => { xml += `${ a.toXML(indent) }\n`; }); this._annotations.filter(a => a._edmAttributes.Target).forEach((a) => { xml += `${ a.toXML(indent) }\n`; }); } return xml; } // no $Namespace toJSONattributes(json) { if (this._edmAttributes) { Object.entries(this._edmAttributes).forEach(([ p, v ]) => { if (p !== 'Name' && p !== 'Namespace') json[p[0] === '@' ? p : `$${ p }`] = v; }); } return this.toOpenApiHints(json); } toJSONchildren(json) { // 'edmx:DataServices' should not appear in JSON // Annotations first this._children.filter(c => c._edmAttributes.Term).forEach((c) => { json = { ...json, ...c.toJSON() }; }); json = super.toJSONchildren(json); if (this._annotations.length > 0) { this._annotations.filter(a => a._edmAttributes.Term).forEach((a) => { Object.entries(a.toJSON()).forEach(([ n, v ]) => { json[n] = v; }); }); const jsonAnnotations = Object.create(null); this._annotations.filter(a => a._edmAttributes.Target).forEach((a) => { jsonAnnotations[a._edmAttributes.Target] = a.toJSON(); }); if (Object.keys(jsonAnnotations).length) json.$Annotations = jsonAnnotations; } if (this._actions) { Object.entries(this._actions).forEach(([ actionName, actionArray ]) => { json[actionName] = []; actionArray.forEach((action) => { json[actionName].push(action.toJSON()); }); }); } return json; } } class DataServices extends Node { constructor(v) { super(v); this._schemas = Object.create(null); if (this.v2) this.setXml( { 'm:DataServiceVersion': '2.0' } ); } get kind() { return 'edmx:DataServices'; } registerSchema(fqName, schema) { if (!this._schemas[fqName]) { this._schemas[fqName] = schema; super.append(schema); } } toJSONchildren(json) { // 'edmx:DataServices' should not appear in JSON this._children.forEach((s) => { json[s._edmAttributes.Namespace] = s.toJSON(); }); return json; } } /* <edmx:Edmx> must contain exactly one <edmx:DataServices> with 1..n <edm:Schema> elements may contain 0..n <edmx:Reference> elements For Odata 1.0..3.0 EDMX is an independent container with its own version 1.0. The OData version can be found at the DataServices Version attribute. From OData 4.0 onwards, EDMX is no longer a separate 'container' object but is used for OData exclusively. Therefore the version attribute reflects the OData version */ class Edm extends Node { constructor(version, service) { super(version, { Version: (version[1]) ? '4.0' : '1.0' }); this._service = service; this._defaultRefs = []; const xmlProps = Object.create(null); if (this.v4) { xmlProps['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx'; xmlProps['xmlns:m'] = undefined; xmlProps['xmlns:sap'] = undefined; } else { xmlProps['xmlns:edmx'] = 'http://schemas.microsoft.com/ado/2007/06/edmx'; xmlProps['xmlns:m'] = 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata'; xmlProps['xmlns:sap'] = 'http://www.sap.com/Protocols/SAPData'; } this.setXml(xmlProps); } get kind() { return 'edmx:Edmx'; } getAnnotations(schemaIndex = 0) { if (this._service && this._service._children[schemaIndex]) return this._service._children[schemaIndex]._annotations; return undefined; } setAnnotations(annotations, schemaIndex = 0) { if (this._service && this._service._children[schemaIndex]) this._service._children[schemaIndex]._annotations = annotations; } toJSON() { const schema = this._service._children[0]; const json = Object.create(null); json.$Version = this._edmAttributes.Version; json.$EntityContainer = `${ schema._edmAttributes.Namespace }.${ schema._ec._edmAttributes.Name }`; const referenceJson = Object.create(null); this._defaultRefs.forEach((r) => { referenceJson[r._edmAttributes.Uri] = r.toJSON(); }); this._children.forEach((r) => { referenceJson[r._edmAttributes.Uri] = r.toJSON(); }); if (Object.keys(referenceJson).length) json.$Reference = referenceJson; this._service.toJSONattributes(json); return this._service.toJSONchildren(json); } // all(default), metadata, annotations toXML(what = 'all') { return `<?xml version="1.0" encoding="utf-8"?>\n${ super.toXML('', what) }`; } innerXML(indent, what) { let xml = ''; if (this.v4 || (this.v2 && (what === 'all' || what === 'annotations'))) { this._defaultRefs.forEach((r) => { xml += `${ r.toXML(indent) }\n`; }); } this._children.forEach((e) => { xml += `${ e.toXML(indent) }\n`; }); xml += `${ this._service.toXML(indent, what) }\n`; return xml; } } class Singleton extends Node { toJSONattributes(json) { forEach(this._edmAttributes, (p, v) => { if (p !== 'Name') { if (p === 'EntityType') // it's $Type in json json.$Type = v; else json[p[0] === '@' ? p : `$${ p }`] = v; } }); return json; } toJSONchildren(json) { const jsonNavPropBinding = Object.create(null); this._children.forEach((npb) => { jsonNavPropBinding[npb._edmAttributes.Path] = npb._edmAttributes.Target; }); if (Object.keys(jsonNavPropBinding).length > 0) json.$NavigationPropertyBinding = jsonNavPropBinding; return json; } getDuplicateMessage() { return `EntityType "${ this._edmAttributes.EntityType }"`; } } class EntitySet extends Singleton { // use the _SetAttributes setSapVocabularyAsAttributes(csn) { super.setSapVocabularyAsAttributes(csn, true); } toJSONattributes(json) { // OASIS ODATA-1231 $Collection=true json.$Collection = true; return super.toJSONattributes(json); } } class PropertyRef extends Node { constructor(version, Name, Alias) { super(version, (Alias) ? { Name, Alias } : { Name }); } toJSON() { return this._edmAttributes.Alias ? { [this._edmAttributes.Alias]: this._edmAttributes.Name } : this._edmAttributes.Name; } } class Key extends Node { // keys is an array of [name] or [name, alias] constructor(version, keys) { super(version); if (keys && keys.length > 0) keys.forEach(k => this.append(new PropertyRef(version, ...k))); } toJSON() { const json = []; this._children.forEach(c => json.push(c.toJSON())); return json; } } /* Base class to Action/Function that provides overloaded XML and JSON rendering of parameters and return type. Parameters are _children. _returnType holds the eventually existing ReturnType in V4. In V2 the return type is a direct attribute called ReturnType to the FunctionImport. See comment in class FunctionImport. */ class ActionFunctionBase extends Node { constructor(version, details, csn) { super(version, details, csn); this._returnType = undefined; } innerXML(indent) { let xml = super.innerXML(indent); if (this._returnType !== undefined) xml += `${ this._returnType.toXML(indent) }\n`; return xml; } toJSONchildren(json) { const jsonParameters = []; this._children.forEach(p => jsonParameters.push(p.toJSON())); if (jsonParameters.length > 0) json.$Parameter = jsonParameters; if (this._returnType) json.$ReturnType = this._returnType.toJSON(); return json; } } // FunctionDefinition should be named 'Function', but this would // collide with a method 'Function' of the Istanbul/NYC tool class FunctionDefinition extends ActionFunctionBase { get kind() { return 'Function'; } } class Action extends ActionFunctionBase {} /* FunctionImport is derived from ActionFunctionBase because in V2 Parameters need to be rendered as sub elements to Function Import. The ReturnType property is set in the assembly code above (the invisible returnType is left undefined) */ class FunctionImport extends Node { getDuplicateMessage() { return `Function "${ this._edmAttributes.Name }"`; } } // ActionFunctionBase {} class ActionImport extends Node { getDuplicateMessage() { return `Action "${ this._edmAttributes.Name }"`; } } class TypeBase extends Node { constructor(version, attributes, csn, typeName = 'Type') { // ??? Is CSN still required? NavProp? super(version, attributes, csn); this._typeName = typeName; this._scalarType = undefined; if (this._edmAttributes[typeName] === undefined) { const typeCsn = csn.items?.type ? csn.items : csn; // Complex/EntityType are derived from TypeBase // but have no type attribute in their CSN if (typeCsn.type) { // this thing has a type // check whether this is a scalar type (or array of scalar type) or a named type if (typeCsn.items?.type && isBuiltinType(typeCsn.items.type)) this._scalarType = typeCsn.items; else if (isBuiltinType(typeCsn.type)) this._scalarType = typeCsn; if (this._scalarType) { this._edmAttributes[typeName] = csn._edmType; // CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream // cds-compiler/issues/7835: Only set length for Binary as long as it is // unclear how many bytes a string character represents. // We can't calculate an unambiguous byte stream length for DB dependent // multi-byte characters. if (!(this._edmAttributes[typeName] === 'Edm.Stream' && !( /* scalarType.type === 'cds.String' || */ this._scalarType.type === 'cds.Binary'))) edmUtils.addTypeFacets(this, this._scalarType); } else { // it's either _edmType or type (_edmType only used for explicit binding param) this._edmAttributes[typeName] = typeCsn._edmType || typeCsn.type; } } // CDXCORE-245: // map type to @odata.Type // optionally add @odata { MaxLength, Precision, Scale, SRID } // but only in combination with @odata.Type // Allow to override type only on scalar and undefined types if ((this._scalarType || typeCsn.type == null) && !csn.elements) { const odataType = csn['@odata.Type']; if (odataType) { const td = EdmPrimitiveTypeMap[odataType]; // If type is known, it must be available in the current version // Reason: EDMX Importer may set `@odata.Type: 'Edm.DateTime'` on imported V2 services // Not filtering out this incompatible type here in case of a V4 rendering would // produce an unrecoverable error. if (td && (td.v2 === this.v2 || td.v4 === this.v4)) { this.setEdmAttribute(typeName, odataType); EdmTypeFacetNames.forEach((facetName) => { const facet = EdmTypeFacetMap[facetName]; if (facet.remove) { this.removeEdmAttribute(facetName); this.removeEdmAttribute(facet.extra); } if (td[facetName] !== undefined && (facet.v2 === this.v2 || facet.v4 === this.v4)) { if (this.v2 && facetName === 'Scale' && csn[`@odata.${ facetName }`] === 'variable') this.setXml({ [facet.extra]: true }); else this.setEdmAttribute(facetName, csn[`@odata.${ facetName }`]); } }); } } } } // Set the collection property if this is either an element, parameter or a term def this.$isCollection = (csn.kind === undefined || csn.kind === 'annotation') ? csn.$isCollection : false; if (options.whatsMySchemaName && this._edmAttributes[typeName]) { const schemaName = options.whatsMySchemaName(this._edmAttributes[typeName]); if (schemaName && schemaName !== options.serviceName) this._edmAttributes[typeName] = this._edmAttributes[typeName].replace(`${ options.serviceName }.`, ''); } // store undecorated type for JSON this._type = this._edmAttributes[typeName]; // decorate for XML (not for Complex/EntityType) if (this.$isCollection && this._edmAttributes[typeName]) this._edmAttributes[typeName] = `Collection(${ this._edmAttributes[typeName] })`; } toJSONattributes(json) { // $Type Edm.String, $Nullable=false MAY be omitted // @ property and parameter for performance reasons if (this._type !== 'Edm.String' && this._type) // Edm.String is default) json[`$${ this._typeName }`] = this._type; if (this._edmAttributes) { Object.entries(this._edmAttributes).forEach(([ p, v ]) => { if (p !== 'Name' && p !== this._typeName && // remove this line if Nullable=true becomes default !(p === 'Nullable' && !v)) json[p[0] === '@' ? p : `$${ p }`] = v; }); } if (this.$isCollection) json.$Collection = this.$isCollection; return this.toOpenApiHints(json); } } class ComplexType extends TypeBase { constructor(version, details, csn) { super(version, details, csn); if (this.v4 && !!csn['@open']) this._edmAttributes.OpenType = true; } } class EntityType extends ComplexType { constructor(version, details, properties, csn) { super(version, details, csn); this.append(...properties); const aliasXref = Object.create(null); csn.$edmKeyPaths.forEach((p) => { const [ alias, ...tail ] = p[0].split('/').reverse(); if (aliasXref[alias] === undefined) aliasXref[alias] = 0; else aliasXref[alias]++; // if it's a path, push the alias if (tail.length > 0) p.push(alias); }); csn.$edmKeyPaths.slice().reverse().forEach((p) => { let alias = p[1]; if (alias) { const c = aliasXref[alias]--; // Limit Key length to 32 characters if (c > 0) { if (alias.length > 28) alias = `${ alias.substr(0, 13) }__${ alias.substr(alias.length - 13, alias.length) }`; alias = `${ alias }_${ c.toString().padStart(3, '0') }`; } else if (alias.length > 32) { alias = `${ alias.substr(0, 15) }__${ alias.substr(alias.length - 15, alias.length) }`; } p[1] = alias; } }); if (csn.$edmKeyPaths && csn.$edmKeyPaths.length) this._keys = new Key(version, csn.$edmKeyPaths); else this._keys = undefined; if (this._openApiHints) { if (csn['@cds.autoexpose']) this._openApiHints['@cds.autoexpose'] = true; if (csn['@cds.autoexposed']) this._openApiHints['@cds.autoexposed'] = true; } } innerXML(indent) { let xml = ''; if (this._keys) xml += `${ this._keys.toXML(indent) }\n`; return xml + super.innerXML(indent); } toJSONattributes(json) { super.toJSONattributes(json); if (this._jsonOnlyAttributes) { Object.entries(this._jsonOnlyAttributes).forEach(([ p, v ]) => { json[p[0] === '@' ? p : `$${ p }`] = v; }); } if (this._keys) json.$Key = this._keys.toJSON(); return json; } } class Term extends TypeBase { constructor(version, attributes, csn) { super(version, attributes, csn); const appliesTo = csn['@odata.term.AppliesTo']; if (appliesTo) this.setEdmAttribute('AppliesTo', Array.isArray(appliesTo) ? appliesTo.join(' ') : appliesTo); } } class TypeDefinition extends TypeBase { constructor(version, attributes, csn) { super(version, attributes, csn, 'UnderlyingType'); } toJSONattributes(json) { super.toJSONattributes(json); json.$UnderlyingType = this._type; return json; } } class Member extends Node { toJSONattributes(json) { json[this._edmAttributes.Name] = this._edmAttributes.Value; return super.toOpenApiHints(json); } } class EnumType extends TypeDefinition { constructor(version, attributes, csn) { super(version, attributes, csn); // array of enum not yet allowed const enumValues = /* (csn.items && csn.items.enum) || */ csn.enum; if (enumValues) { Object.entries(enumValues).forEach(([ en, e ]) => { this.append(new Member(version, { Name: en, Value: e.val } )); }); } } toJSONchildren(json) { this._children.forEach(c => c.toJSONattributes(json)); return json; } } class PropertyBase extends TypeBase { constructor(version, attributes, csn) { super(version, attributes, csn); this._csn = csn; if (this.v2) { const typecsn = csn.items || csn; // see edmUtils.mapsCdsToEdmType => add sap:display-format annotation // only if Edm.DateTime is the result of a cast from Edm.Date // but not if Edm.DateTime is the result of a regular cds type mapping if (this._edmAttributes.Type === 'Edm.DateTime' && (typecsn.type !== 'cds.DateTime' && typecsn.type !== 'cds.Timestamp')) this.setXml( { 'sap:display-format': 'Date' } ); } this.setNullable(); } setNullable() { // From the Spec: In OData 4.01 responses a collection-valued property MUST specify a value for the Nullable attribute. if (this.$isCollection) this._edmAttributes.Nullable = !this.isNotNullable(); // Nullable=true is default, mention Nullable=false only in XML // Nullable=false is default for EDM JSON representation 4.01 // When a key explicitly (!) has 'notNull = false', it stays nullable else if (this.isNotNullable()) this._edmAttributes.Nullable = false; } isNotNullable(csn = undefined) { const nodeCsn = csn || this._csn; // Nullable=true is default, mention Nullable=false only in XML // Nullable=false is default for EDM JSON representation 4.01 // When a key explicitly (!) has 'notNull = false', it stays nullable return (nodeCsn._NotNullCollection !== undefined ? nodeCsn._NotNullCollection : (nodeCsn.key && nodeCsn.notNull !== false) || nodeCsn.notNull === true); } toJSONattributes(json) { super.toJSONattributes(json); // mention all nullable elements explicitly, remove if Nullable=true becomes default if (this._edmAttributes.Nullable === undefined || this._edmAttributes.Nullable === true) json.$Nullable = true; return json; } } /* ReturnType is only used in v4, mapCdsToEdmType can be safely called with V2=false */ class ReturnType extends PropertyBase { constructor(version, csn) { super(version, {}, csn); // CSDL 12.8: If the return type is a collection of entity types, // the Nullable attribute has no meaning and MUST NOT be specified. if (csn.$NoNullableProperty) delete this._edmAttributes.Nullable; } // we need Name but NO $kind, can't use standard to JSON() toJSON() { const json = Object.create(null); this.toJSONattributes(json); // CSDL 12.8: If the return type is a collection of entity types, // the Nullable attribute has no meaning and MUST NOT be specified. if (this._csn.$NoNullableProperty) delete json.$Nullable; return json; } } class Property extends PropertyBase { constructor(version, attributes, csn) { super(version, attributes, csn); // TIPHANACDS-4180 if (this.v2) { if (csn['@odata.etag'] || csn['@cds.etag']) this._edmAttributes.ConcurrencyMode = 'Fixed'; // translate the following @sap annos as xml attributes to the Property forEach(csn, (p, v) => { if (p in Property.SAP_Annotation_Attributes) this.setXml( { [`sap:${ p.slice(5).replace(/\./g, '-') }`]: v }); }); } // OData only allows simple values, no complex expressions or function calls // This is a poor man's expr renderer, assuming that edmPreprocessor has // added a @Core.ComputedDefaultValue for complex defaults if (csn.default && !csn['@Core.ComputedDefaultValue']) { const def = csn.default; // if def has a value, it's a simple value let defVal = def.val; // if it's a simple value with signs, produce a string representation if (csn.default.xpr) { defVal = csn.default.xpr.map((i) => { if (i.val !== undefined) { if (csn.type === 'cds.Boolean') return i.val ? 'true' : 'false'; return i.val; } return i; }).join(''); } // complex values should be marked with @Core.ComputedDefaultValue already in the edmPreprocessor if (this.v4 && defVal !== undefined) { /* No Default Value rendering in V2 (or only with future flag). Reason: Fiori UI5 expects 'Default' under extension namespace 'sap:' Additionally: The attribute is named 'Default' in V2 and 'DefaultValue' in V4 */ this._edmAttributes[`Default${ this.v4 ? 'Value' : '' }`] = defVal; } } } // required for walker to identify property handling.... // static get isProperty() { return true } } // the annotations in this array shall become exposed as Property attributes in // the V2 metadata.xml Property.SAP_Annotation_Attributes = { '@sap.hierarchy.node.for': 1, // -> sap:hierarchy-node-for '@sap.hierarchy.parent.node.for': 1, // -> sap:hierarchy-parent-node-for '@sap.hierarchy.level.for': 1, // -> sap:hierarchy-level-for '@sap.hierarchy.drill.state.for': 1, // -> sap:hierarchy-drill-state-for '@sap.hierarchy.node.descendant.count.for': 1, // -> sap:hierarchy-node-descendant-count-for '@sap.parameter': 1, }; class Parameter extends PropertyBase { constructor(version, attributes, csn = {}, mode = null) { super(version, attributes, csn); if (mode != null) this._edmAttributes.Mode = mode; // V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true // V2 XML Spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let // the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor if (this.v2 && this._edmAttributes.Nullable === undefined) this.setXml({ Nullable: true }); } toJSON() { // we need Name but NO $kind, can't use standard to JSON() const json = Object.create(null); json.$Name = this._edmAttributes.Name; return this.toJSONattributes(json); } } class NavigationPropertyBinding extends Node {} class OnDelete extends Node {} class ReferentialConstraint extends Node { constructor(version, attributes, csn) { super(version, attributes, csn); this._d = null; this._p = null; } innerXML(indent) { if (this._d && this._p) return `${ this._p.toXML(indent) }\n${ this._d.toXML(indent) }\n`; return super.innerXML(indent); } } class NavigationProperty extends Property { constructor(version, attributes, csn) { super(version, attributes, csn); const [ src, tgt ] = edmUtils.determineMultiplicity(csn._constraints._partnerCsn || csn); csn._constraints._multiplicity = csn._constraints._partnerCsn ? [ tgt, src ] : [ src, tgt ]; this._type = attributes.Type; this.$isCollection = this.isToMany(); this._targetCsn = csn._target; if (this.v4) { if (options.isStructFormat && this._csn.key) this._edmAttributes.Nullable = false; // either csn has multiplicity or we have to use the multiplicity of the backlink if (this.$isCollection) { this._edmAttributes.Type = `Collection(${ attributes.Type })`; // attribute Nullable is not allowed in combination with Collection (see Spec) // Even if min cardinality is > 0, remove Nullable, because the implicit OData contract // is that a navigation property must either return an empty collection or all collection // values are !null (with other words: a collection must never return [1,2,null,3]) delete this._edmAttributes.Nullable; } // we have exactly one selfReference or the default partner if ( !csn.$noPartner) { const partner = csn._selfReferences.length === 1 ? csn._selfReferences[0] : csn._constraints._partnerCsn; if (partner && partner['@odata.navigable'] !== false && this._csn._edmParentCsn.kind !== 'type') { // $abspath[0] is main entity this._edmAttributes.Partner = partner.$abspath.slice(1).join('/'); } } /* 1) If this navigation property belongs to an EntityType for a parameterized entity ```entity implemented in calcview (P1: T1, ..., Pn: Tn) { ... }``` and if the csn.containsTarget for this NavigationProperty is true, then this is the generated 'Results' association to the underlying entityType. Only this special association may have an explicit ContainsTarget attribute. See csn2edm.createEntityTypeAndSet() for details 2) ContainsTarget stems from the @odata.contained annotation */ if (csn['@odata.contained'] || csn.containsTarget) this._edmAttributes.ContainsTarget = true; if (this._edmAttributes.ContainsTarget === undefined && csn.type === 'cds.Composition') { // Delete is redundant in containment // TODO: to be specified via @sap.on.delete this.append(new OnDelete(version, { Action: 'Cascade' } ) ); } } if (this.v2 && this.isNotNullable()) { // in V2 not null must be expressed with target cardinality of 1 or more, // store Nullable=false and evaluate in determineMultiplicity() delete this._edmAttributes.Nullable; } // A nav prop has no default value delete this._edmAttributes.DefaultValue; // store NavProp reference in the model for bidirectional $Partner tagging (done in getReferentialConstraints()) csn._NavigationProperty = this; } // if the backlink association is annotated with @odata.contained or the underlying association // is marked with _isToContainer, then the association is a Containment relationship isContainment() { return this._csn._isToContainer || this._csn['@odata.contained']; } isNotNullable(csn = undefined) { const nodeCsn = csn || this._csn; // Set Nullable=false only if 'NOT NULL' was specified in the model // Do not derive Nullable=false from key attribute. // OR if an association has cardinality.min > 0 // If this is a backlink ($self = <from>.<to>) _partnerCsn.cardinality.srcmin > 0 if available // notNull is evaluated for non assoc elements only! // A managed association with unspecified cardinality that is to not null // is effectively a to-min-1 relationship as there must be a value for // the foreign keys (they are not null as well). // During the foreign key generation the minimum cardinality of such an association // is set to 1 as this property is available in the OData CSN. const tgtCard = edmUtils.getEffectiveTargetCardinality(nodeCsn); return (nodeCsn.notNull === true && !nodeCsn.target || tgtCard.min > 0); } isToMany() { return (this.$isCollection || this._csn._constraints._multiplicity[1] === '*'); } toJSONattributes(json) { // use the original type, not the decorated one super.toJSONattributes(json); json.$Type = this._type; // attribute Nullable is not allowed in combination with Collection (see Spec) if (json.$Collection) delete json.$Nullable; return json; } toJSONchildren(json) { const jsonConstraints = Object.create(null); this._children.forEach((c) => { switch (c.kind) { case 'ReferentialConstraint': // collect ref constraints in dictionary jsonConstraints[c._edmAttributes.Property] = c._edmAttributes.ReferencedProperty; break; case 'OnDelete': json.$OnDelete = c._edmAttributes.Action; break; default: throw new CompilerAssertion(`Debug me: Unhandled NavProp child: ${ c.kind }`); } }); // TODO Annotations if (Object.keys(jsonConstraints).length > 0) json.$ReferentialConstraint = jsonConstraints; return json; } // V4 referential constraints! addReferentialConstraintNodes() { // flip the constrains if this is a $self partner let { _constraints } = this._csn; let [ i, j ] = [ 0, 1 ]; if (this._csn._constraints._partnerCsn) { _constraints = this._csn._constraints._partnerCsn._constraints; [ i, j ] = [ 1, 0 ]; } if (_constraints.constraints) { Object.values(_constraints.constraints) .forEach(c => this.append( new ReferentialConstraint(this._v, { Property: c[i].join(options.pathDelimiter), ReferencedProperty: c[j].join(options.pathDelimiter), } ) )); } } } // Annotations below class AnnotationBase extends Node { // No Kind: AnnotationBase is base class for Thing and ValueThing with dynamic kinds, // this requires an explicit constructor as the kinds cannot be blacklisted in // Node.toJSON() toJSON() { const json = Object.create(null); this.toJSONattributes(json); return this.toJSONchildren(json); } toJSONattributes(json) { return super.toJSONattributes(json, false); } getConstantExpressionValue() { // short form: key: value const inlineConstExpr = [ 'Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid', 'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte', 'Edm.Single', 'Edm.Stream', 'Edm.String', 'Edm.TimeOfDay', // Edm.Geo* according to https://issues.oasis-open.org/browse/ODATA-1323 /* 'Edm.Geography', 'Edm.GeographyPoint', 'Edm.GeographyLineString', 'Edm.GeographyPolygon', 'Edm.GeographyMultiPoint', 'Edm.GeographyMultiLineString', 'Edm.GeographyMultiPolygon', 'Edm.GeographyCollection', 'Edm.Geometry', 'Edm.GeometryPoint', 'Edm.GeometryLineString', 'Edm.GeometryPolygon', 'Edm.GeometryMultiPoint', 'Edm.GeometryMultiLineString', 'Edm.GeometryMultiPolygon', 'Edm.GeometryCollection', */ /* UI.xml: defines Annotations with generic type 'Edm.PrimitiveType' */ 'Edm.PrimitiveType', 'Edm.Untyped', 'Bool', // Official JSON V4.01 Spec defines these paths as constant inline expression: 'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath' ]; const dict = this._jsonOnlyAttributes; const inline = edmUtils.intersect(Object.keys(dict), inlineConstExpr); if (inline.length === 1) { const v = dict[inline[0]]; /* short notation for Edm.Boolean, Edm.String and Edm.Float, see internal project: edmx2csn-npm/edm-converters/blob/835d92a1aa6b0be25c56cef85e260c9188187429/lib/edmxV40ToJsonV40/README.md */ if (inline[0] === 'Edm.Boolean') { if (v === 'true') return true; return (v === 'false') ? false : v; } return v; } // if this is not a constant expression shortcut, render key/value pair verbatim // without filtering non-spec-compliant constExpr const json = Object.create(null); Object.entries(dict).forEach(([ k, v ]) => { json[`$${ k }`] = v; }); return json; } mergeJSONAnnotations(prefix = '') { return this._children.filter(c => c.kind === 'Annotation').reduce((o, a) => { Object.entries(a.toJSON()).forEach(([ n, v ]) => { o[prefix + n] = v; }); return o; }, Object.create(null)); } } class Annotations extends AnnotationBase { constructor(version, target) { super(version, { Target: target }); if (this.v2) this._xmlOnlyAttributes.xmlns = 'http://docs.oasis-open.org/odata/ns/edm'; } toJSONattributes(json) { forEach(this._edmAttributes, (p, v) => { if (p !== 'Target') json[p[0] === '@' ? p : `$${ p }`] = v; }); return json; } toJSONchildren(json) { this._children.forEach((a) => { Object.entries(a.toJSON()).forEach(([ n, v ]) => { json[n] = v; }); }); return json; } } // An Annotation must contain either children or a constant value // The value attribute is rendered by getConstantExpressionValue(). // However, in case the constant expression value differs for XML an JSON // (EnumMember & EnumMember@odata.type) then the value properties must // be separated by using setJSON(attribute) and setXML(attribute). // See genericTranslation::handleValue() for details (especially the code // that sets the EnumMember code). All this has been done because the // Annotation object is passed around in genericTranslation and the // properties are set all over the place. The initial assumption was that // the constant expression value is the same for both XML and JSON. But // since it was discovered, that in JSON the EnumMember type must be // transported this is no longer the case.... class Annotation extends AnnotationBase { constructor(version, termName, ...children) { super(version, { Term: termName } ); this.append(...children); } toJSON() { const json = super.mergeJSONAnnotations(this.getJsonFQTermName()); const e = this._children.filter(c => c.kind !== 'Annotation'); if (e.length === 0 || this._ignoreChildren) // must be a constant expression json[this.getJsonFQTermName()] = this.getConstantExpressionValue(); else // annotation must have exactly one child (=record or collection) json[this.getJsonFQTermName()] = e[0].toJSON(); return json; } getJsonFQTermName() { const qualifier = this._edmAttributes.Qualifier ? `#${ this._edmAttributes.Qualifier }` : ''; return `@${ this._edmAttributes.Term }${ qualifier }`; } } class Collection extends AnnotationBase { constructor(version, ...children) { super(version); this.append(...children); } toJSON() { // EDM JSON doesn't mention annotations on collections return this._children.map(a => a.toJSON()); } } class Record extends AnnotationBase { constructor(version, ...children) { super(version); this.append(...children); } toJSONattributes(json) { if (this._jsonOnlyAttributes.Type) json['@type'] = this._jsonOnlyAttributes.Type; const keys = Object.keys(this._edmAttributes).filter(k => k !== 'Type'); for (const key of keys) json[`$${ key }`] = this._edmAttributes[key]; return json; } toJSONchildren(json) { this._children.forEach((c) => { switch (c.kind) { case 'Annotation': { Object.entries(c.toJSON()).forEach(([ n, v ]) => { json[n] = v; }); break; } case 'PropertyValue': { // plus property annotations as [a.Property]@anno: val Object.entries(c.mergeJSONannotations()).forEach(([ n, a ]) => { json[n] = a; }); // render property as const expr (or subnode) json[c._edmAttributes.Property] = c.toJSON(); break; } default: throw new CompilerAssertion(`Debug me: Unhandled Record child: ${ c.kind }`); } }); return json; } } class PropertyValue extends AnnotationBase { constructor(version, property) { super(version); this._edmAttributes.Property = property; } toJSON() { const children = this._children.filter(child => child.kind !== 'Annotation'); if (children.length === 0 || this._ignoreChildren) return this.getConstantExpressionValue(); return children[0].toJSON(); } mergeJSONannotations() { return super.mergeJSONAnnotations(this._edmAttributes.Property); } } class Thing extends AnnotationBase { constructor(version, kind, details) { super(version, details); this._kind = kind; } get kind() { return this._kind; } } class ValueThing extends Thing { constructor(version, kind, value) { super(version, kind, undefined); this._value = value; } toXML(indent = '') { const { kind } = this; let xml = `${ indent }<${ kind }${ this.toXMLattributes() }`; xml += (this._value !== undefined ? `>${ edmUtils.escapeStringForText(this._value) }</${ kind }>` : '/>'); return xml; } toJSON() { if (this._children.length === 0 || this._ignoreChildren) // must be a constant expression return this.getConstantExpressionValue(); return this._children[0].toJSON(); } } // Binary/Unary dynamic expression class Expr extends Thing { toJSON() { // toJSON: depending on number of children unary or n-ary expr const json = this.mergeJSONAnnotations(); const e = this._children.filter(c => c.kind !== 'Annotation'); if (e.length === 1) json[`$${ this.kind }`] = e[0].toJSON(); else json[`$${ this.kind }`] = e.map(c => c.toJSON()); return json; } } class Null extends AnnotationBase { toXMLattributes() { return ''; } toJSON() { const json = this.mergeJSONAnnotations(); json[`$${ this.kind }`] = null; return json; } } class Apply extends AnnotationBase { toJSON() { const json = this.mergeJSONAnnotations(); json[`$${ this.kind }`] = this._children.filter(c => c.kind !== 'Annotation').map(c => c.toJSON()); return this.toJSONattributes(json); } } class Cast extends AnnotationBase { toXMLattributes() { if (this._jsonOnlyAttributes.Collection) { const ot = this._edmAttributes.Type; this._edmAttributes.Type = `Collection(${ ot })`; const str = super.toXMLattributes(); this._edmAttributes.Type = ot; return str; } return super.toXMLattributes(); } toJSON