Use enum values in your CAP service implementation

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.