Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
3,547
I like a lot, that SAP Cloud Application Programming Model (CAP) supports enumerations in data modeling (ref documentation). In my opinion, enumerations are an easy way to bring more semantic in the data model and write code easier to read and therefore to maintain. Unfortunately, using enumeration in service implementation is not possible yet in CAP.

What is the problem?


To illustrate the problem, let's take the bookshop example. Instead of ordering books based on their inventory, let order based on their stock status. Each book has a stock status. When a book is in stock, it can be ordered. Otherwise the order cannot be placed. That would lead to extend the data model as following:
type StockStatus: String enum {
InStock = 'I';
OutOfStock = 'O';
};
entity Books: [...] {
[...]
stockStatus: StockStatus;
};

 

The service implementation needs to be adapted as well to retrieve the stock status and check its value. Ideally, we want to check against the declared enumeration value (aka. InStock) and not the value set behind the enumeration value (aka. I) as it could change over time. The code should consequently look like this:
class CatalogService extends cds.ApplicationService {
init() {
this.on('submitOrder', async (req) => {
const { book, quantity } = req.data
const { stockStatus } = await SELECT `stockStatus` .from (Books,book)
if (stockStatus == StockStatus.InStock) {
[...]
} else {
[...]
}
});
[...]
return this.init();
}
}

Unfortunately, this code won't execute as enumerations are not exposed and there is no public API available as of now in CAP to access them easily. We could create a StockStatus module like this:
module.exports = {
InStock: 'I',
OutOfStock: 'O'
};

and add const StatusCode = require('./StockStatus'); to the service implementation to achieve it but it still error-prone as developers need to carefully align values in the module with the values used in the enumeration definition.

What is the solution?


While I'm confident the CAP product team is going to address this problem in the future, I decided to look for my own solution for the time being with the following requirements:

  • The solution should be generic and not service implementation specific

  • The solution should be easily pluggable, so that it can be removed with minimum of efforts once the CAP product team releases their official solution


My idea was therefore to extend the cds facade with a new method called enums(), returning the enumerations like the entities() method. The bookshop service implementation would then look like this:
const cds = require('@sap/cds')
const { Books } = cds.entities('sap.capire.bookshop')
const { StockStatus } = cds.enums('sap.capire.bookshop')

class CatalogService extends cds.ApplicationService {
init() {
this.on('submitOrder', async (req) => {
[...]
if (stockStatus == StockStatus.InStock) {
[...]
} else {
[...]
}
});
[...]
return this.init();
}
}

How can it be implemented? First, when the platform bootstraps, it is possible to register a callback invoked once the model is loaded (see documentation). That will provide us access to all type definitions, including enumerations. We can extract the enumerations and register the enums() method in the cds facade to return the enumerations. Since it is theoretically also possible to define enumerations in a service, we also need an enums() method in the cds.Service class.
const cds = require('@sap/cds')

cds.on('loaded', (csn) => {
const enums = Object.keys(csn.definitions)
.filter((definitionName) => typeof csn.definitions[definitionName].enum === 'object')
.reduce((foundEnums, enumName) => {
const enumValues = {};
const enumDefinition = csn.definitions[enumName].enum;
for (let enumValueName of Object.keys(enumDefinition)) {
const enumValueDefinition = enumDefinition[enumValueName];
if (typeof enumValueDefinition === 'object' && Object.keys(enumValueDefinition).includes('val')) {
enumValues[enumValueName] = enumDefinition[enumValueName].val;
} else {
enumValues[enumValueName] = enumValueName;
}
}
foundEnums[enumName] = new Proxy(enumValues, {
get: (target, name, receiver) => {
if (Reflect.has(target, name)) {
return target[name];
} else {
throw new Error(`Enumeration '${enumName}' does not define value '${name}'`);
}
},
set: (target, name, receiver) => {
throw new Error(`Enumeration '${enumName}' cannot be modified`);
}
});
return foundEnums;
}, {});
const findEnumsByNamespace = (namespace) => {
if (!namespace) {
return enums;
} else {
return Object.keys(enums)
.filter((enumName) => enumName.length > namespace.length && enumName.substring(0, namespace.length) == namespace)
.reduce((filteredEnums, enumName) => {
const packageEndIndex = enumName.lastIndexOf('.', namespace.length);
const enumSimpleName = packageEndIndex < 0 ? enumName : enumName.substring(packageEndIndex + 1);
filteredEnums[enumSimpleName] = enums[enumName];
return filteredEnums;
}, {});
}
}
cds.extend(cds.__proto__).with({
enums: (namespace) => {
return findEnumsByNamespace(namespace || cds.db.namespace);
}
});
cds.extend(cds.Service).with(class {
enums(namespace) {
return findEnumsByNamespace(namespace || this.namespace);
}
});
});

Copy & paste this code in your server.js or srv/server.js file and the magic happens.

Any limitation?


I am aware of at least one. Only enumerations used in entities are exposed in the model. For example, if you remove the stockStatus attribute in the Book entity, the StatusCode enumeration is not referenced anywhere and cds.enums() will not return it, although it is defined.

Conclusion


Not being able to use declared enumerations in the service implementation looks at a first sight like a big limitation. However, it is very easy to fix (almost) without hacking the platform. Feel free to use it, like or dislike it and share your feedback. I'm sure it will be at the end very valuable for the CAP product team and us at the end.
7 Comments
0 Kudos

Very efficiently written information. It will be beneficial to anybody who utilizes it, including me. Keep up the good work. For sure i will check out more posts. This site seems to get a good amount of visitors

gregorw
Active Contributor
Hi  Alexandre,

thank you for sharing this approach. Would it be possible that you create a NPM package for it?

Best Regards
Gregor
sergei-u-niq
Active Contributor
0 Kudos

very nice!


I usually add a utils.enums object which contains the enums of the project. (so usage is identical to yours) but has the drawback of needing to update two places whenever there is a change...
0 Kudos
Thank you very much for your feedback.
0 Kudos
Hi Gregor,

I'm checking right now internally, how we can best deliver this as NPM package. I will come back hopefully soon with more details.

Regards,
-Alexandre
Attila
Active Participant

Hi Alexandre,

Is maybe the CDS Typer feature for enums released recxently by SAP what we was waiting for ? I'm not so deep in the frw lifecycle yet, but definitely like to avoid double maintenance with constant modules and reworked schema definitions. This CDS Typer looks good at first look, despite still a separate tool.

According to the documentation after the installation, CDS Typer should generate types into folder @cds-models after saving files with .cds extension in the workspace. Prerequisite is to have SAP CDS Language Support extension installed in VScode. I have it, but nothing happened in VSCode or BAS.

However it can be triggered manually with this script from package.json:

"types": "npx @cap-js/cds-typer ./db/schema.cds --outputDirectory ./@cds-models"

This worked, now I can use my enums in the service implementation

   type JobStatus : String(20) enum {
SUCCESS = 'Success';
ERROR = 'Error';
}

 

const { JobStatus } = require('#cds-models/data');

 

Thank You

0 Kudos
Hi Attila,

Yes, it definitely looks like what I was looking for and the workaround proposed in the blog is no more needed. Thanks for taking the time to point that out and share the approach with the community!