@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
320 lines (292 loc) • 10.8 kB
JavaScript
//
// 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
/**
*
* @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
}