UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

320 lines (292 loc) 10.8 kB
// // input: single (interop) CSN for a Data Product, including texts // to be clarified: should/does it already contain a service wrapper? // service name (incl. namespace) for step 5 // output: single CSN file with ... // 1) all type refs resolved to built-in types // 2) all type definitions removed // 3) unwanted annotations removed, some annotations renamed // 4) unused texts removed // 5) wrap everything into service if not yet done, service annos added // 6) all annotations for entities/elements moved from "definitions" into "extensions" // 7) add java name annotations // // Assumptions on input CSN (reflecting current possibilities of interop CSN): // - definitions section only contains // - zero or one service // - entities // - if there is a service, all entities are part of this service // - no views/projections // - no actions/functions // - no parameters // - type definitions // - type definitions are only simple (scalar) types referring to built-in types // - no structs, no arrays, no assocs // - can be enum type // - no type chains // - entity elements can be // - built-in types, including enum types // - references to the simple (scalar) type definitions described above // - unmanaged(!) associations // - no structs, no arrays 'use strict' /** * * @param csn * @param srvName * @param ordId */ exports.prepareCsn = function (csn, srvName, ordId) { // // ---------- step 0: check assumptions --------------------------------------------------------- // // TODO: check whether all assumptions are met, raise error if not // // ---------- step 1: resolve simple types ------------------------------------------------------ // // Loop over all entity elements. If type is reference to simple type, replace it by // corresponding built-in type and copy over the type's properties (unless already present) for (let n in csn.definitions) { let entity = csn.definitions[n] if (entity.kind === 'entity' && entity.elements) { for (let en in entity.elements) { let element = entity.elements[en] if (element.type && !element.type.startsWith('cds.')) { let type = csn.definitions[element.type] // TODO: warn/err if type not found if (type?.kind === 'type') { element.type = type.type // copy props of type if not yet present for (let prop in type) { if (prop !== 'type' && prop !== 'kind' && !element[prop]) { element[prop] = type[prop] } } } } } } } // // ---------- step 2: remove simple types --------------------------------------------------------- // // Remove all type definitions. for (let n in csn.definitions) { let typedef = csn.definitions[n] if (typedef.kind === 'type') { delete csn.definitions[n] } } // // ---------- step 3: remove or rename annotations ------------------------------------------------ // const annosToRemove = [ '__abapOriginalName', '@AccessControl.authorizationCheck', '@AccessControl.personalData', '@Analytics.dataCategory', '@Analytics.dataExtraction', '@Metadata.allowExtensions', '@ObjectModel.sapObjectNodeType.name', '@ObjectModel.usageType', '@VDM.viewType', '@AbapCatalog', '@ObjectModel.upperCase' ] const annotationsToRename = { '@EndUserText.label': '@title' } /** * * @param obj */ function removeOrRenameAnnotations(obj) { for (let prop in obj) { // if prop has any entry in annosToRemove as prefix, delete it for (let anno of annosToRemove) { if (prop.startsWith(anno)) { delete obj[prop] break } } // if prop equals any entry in annosToRename, rename it if (annotationsToRename[prop]) { obj[annotationsToRename[prop]] = obj[prop] delete obj[prop] } } } for (let n in csn.definitions) { let entity = csn.definitions[n] if (entity.kind === 'entity') { removeOrRenameAnnotations(entity) for (let en in entity.elements || {} ) { removeOrRenameAnnotations(entity.elements[en]) } } } // // ---------- step 4: remove unused texts --------------------------------------------------------- // // Remove all texts that are no longer used. // Usage example: "@EndUserText.label": "{i18n>I_CUSTOMERCOMPANY@ENDUSERTEXT.LABEL}" // first loop over entities and their elements and collect all i18n keys used in annotation values ... let i18nKeys = new Set() /** * * @param obj */ function CollectKeys(obj) { for (let prop in obj) { if (prop.startsWith('@') && typeof obj[prop] === 'string' && obj[prop].startsWith('{i18n>')) { let val = obj[prop] i18nKeys.add(val.substring(6, val.length - 1)) // extract text key } } } for (let n in csn.definitions) { let entity = csn.definitions[n] if (entity.kind === 'entity') { CollectKeys(entity) for (let en in entity.elements || {} ) { CollectKeys(entity.elements[en]) } } } // ... then remove all texts that are not used for (let lang in csn.i18n) { for (let key in csn.i18n[lang]) { if (!i18nKeys.has(key)) { delete csn.i18n[lang][key] } } } // // ---------- step 5: wrap everyting into service if not yet done ------------------------------- // // This function must be called with a service name "srvName" as argument. if (!srvName) throw new Error('No service name has been provided.') // Assumptions (based on the rules for DP CSNs): // 1) there is no or one context or service in the CSN // 2) if there is a context or service, all objects in the CSN belong to it // // If there is no context/service (this is a DP CSN coming from BAH): // put all entities into a new service "srvName", i.e. // - prefix all object and association target names with "srvName" // - add a service "srvName" with CAP specific annotations for DP handling // If there is a context/service (DP CSN comes from UCL, context/service name is the schema name in the HDLFS share): // move all entities out of the context/service and put them into a new service "srvName", i.e. // - remove the context/service // - change the prefix for all entity and association target names from // the old context/service name to "srvName" // - add a service "srvName" with CAP specific annotations for DP handling // // Note: Currently an existing context/service doesn't contain further info (e.g. annos), // so deleting it is ok. let ctxName = null; const ctxs = Object.entries(csn.definitions).filter(([,o]) => o.kind === 'context' || o.kind === 'service') if (ctxs.length > 0) { // check assumption 1 if (ctxs.length > 1) throw new Error('Data Product CSN is invalid: it has more than one context or service.') ctxName = ctxs[0][0] delete csn.definitions[ctxName] // check assumption 2 const bad = Object.keys(csn.definitions).filter((n) => !n.startsWith(ctxName + '.')) if (bad.length > 0) throw new Error(`Unexpected entity name "${bad[0]}" without prefix in Data product CSN with context "${ctxName}"`) } function changePrefix(name) { if (ctxName) { name = name.substring(ctxName.length + 1) // remove prefix incl. '.' } return srvName + '.' + name } // loop over all definitions and prefix names with service name // if ctxName accidentially equals srvName, do nothing if (ctxName != srvName) { for (let n in csn.definitions) { let def = csn.definitions[n] // adapt assoc targets for (let en in def.elements || {} ) { let element = def.elements[en] if (element?.type === 'cds.Association' || element?.type === 'cds.Composition') { element.target = changePrefix(element.target) } } csn.definitions[changePrefix(n)] = def delete csn.definitions[n] } } // add the service csn.definitions[srvName] = { kind : 'service', '@cds.dp.ordId': ordId, '@cds.external': true, '@data.product': true, '@protocol' : 'none' // don't serve via OData }; if (ctxName) csn.definitions[srvName]['@data.product.schema'] = ctxName; // // ---------- step 6: move entity/element annotations into extensions section ------------------- // for (let n in csn.definitions) { let entity = csn.definitions[n] if (entity.kind === 'entity') { let annosEntity = { annotate: n } for (let prop in entity) { if (prop.startsWith('@')) { annosEntity[prop] = entity[prop] delete entity[prop] } } for (let en in entity.elements || {} ) { let element = entity.elements[en] let annosElement = {} for (let prop in element) { if (prop.startsWith('@')) { annosElement[prop] = element[prop] delete element[prop] } } if (Object.keys(annosElement).length > 0) { annosEntity.elements ||= {} annosEntity.elements[en] = annosElement } } if (Object.keys(annosEntity).length > 1) { csn.extensions ||= [] csn.extensions.push(annosEntity) } } } // // ---------- step 7: add Java specific annotations -------------------------------------------- // // In S/4 DPs, names of association elements oftentimes begin with `_`, and the respective FK // has the same name w/o `_`, like: // * Customer : ... // the FK // * _Customer : Association ... // When generating accessor classes, CAP Java ignores the `_` prefix, which leads to duplicate names. // // If an association-like element's name starts with `_` and there is an element with the same name w/o '_', // we add an annotation to change the Java name. // This is done after the separation of all other entities into a separate file, so that these // annotations are visible directly in the entity definition for (let n in csn.definitions) { let entity = csn.definitions[n] if (entity.kind === 'entity') { // find offensive names for (let en in entity.elements || {} ) { let element = entity.elements[en] if (en.startsWith('_') && entity.elements[en.slice(1)] && element?.type == 'cds.Association' || element?.type == 'cds.Composition') { element['@cds.java.name'] = 'to' + en.slice(1) } } } } // // ---------------------------------------------------------------------------------------------- // return csn }