
Hi all,
Many of us are leveraging SAP CAP (Node.js with TypeScript) for our projects.
While CAP provides an abstract mechanism to derive RESTful oData services given a CDS data model there's always room for more efficiency and improvement in the implementing communication between business logic, service, and persistence layer.
-
1. CDS-TS-Dispatcher: Simplifying SAP CAP TypeScript Development
2. CDS-TS-Repository: Simplify SAP CAP Entity persistance with BaseRepository
Crafted by developers for developers by ABS & DxFrontier team
-
Before we dive in, you should have a basic understanding of CDS-TS-Dispatcher: Simplifying SAP CAP TypeScript Development
This pattern provides a structured approach to organizing the various components of our SAP CAP applications, promoting modularity, scalability, and maintainability.
Each layer should be structured based on domain of responsabilities like :
Controller Service Repository design pattern
Now we will create the Controller layer and for that we created a new class BookHandler which is annotated with the @EntityHandler(Book) decorator.
@EntityHandler decorator takes as an argument the Book entity, this will have effect on all event decorators like @BeforeRead(), @AfterRead() ..., all events will be executed exclusively for this Book entity.
@EntityHandler(Book)
class BookHandler {
@Inject(SRV) private readonly srv: Service;
@Inject(BookService) private readonly bookService: BookService;
@AfterCreate()
private async afterCreate(@Result() result: Book, @Req() req: Request): Promise<void> {
this.bookService.validateData(result, req);
}
@BeforeRead()
@Use(MiddlewareMethodBeforeRead)
private async beforeRead(@Req() req: TypedRequest<Book>): Promise<void> {
this.bookService.showConsoleLog();
}
@AfterRead()
private async afterRead(
@Req() req: Request,
@Results() results: Book[],
@SingleInstanceSwitch() singleInstance: boolean,
@IsColumnSupplied<Book>('price') hasPrice: boolean,
@IsPresent('SELECT', 'columns') hasColumns: boolean,
@IsRole('Developer', 'AnotherRole') role: boolean,
@GetRequest('locale') locale: Request['locale'],
Promise<void> {
await this.bookService.manageAfterReadMethods({ req, results, singleInstance });
}
@AfterUpdate()
private async afterUpdate(@Result() result: Book, @Req() req: TypedRequest<Book>): Promise<void> {
await this.bookService.addDefaultTitleText(result, req);
}
@AfterDelete()
private async afterDelete(@Result() deleted: boolean, @Req() req: Request): Promise<void> {
this.bookService.notifyItemDeleted(req, deleted);
}
}
export default BookHandler;
@EntityHandler(Book) decorator will registers 5 events :
@AfterCreate()
@BeforeRead()
@AfterRead()
@AfterUpdate()
@AfterDelete()
Every decorator will create the corresponding event in the SAP CAP CDS event registration, this means that the callback of every decorator will be triggered when a REST (CRUD) Request is performed.
@BeforeRead() and @AfterRead() will be triggered when a new GET request is performed :
Example: GET http://localhost:4004/odata/v4/catalog/Book
-
@Inject() dependencies
We inject the BookService class by using Dependendy injection and gaining visibility over all public methods created in the BookService class:
@Inject(BookService) private readonly bookService: BookService
In @ServiceLogic() will reside all of the customer business logic. This @ServiceLogic() will make a connection between @EntityHandler(Book) and @Repository()
@ServiceLogic()
class BookService {
@Inject(SRV) private readonly srv: Service;
@Inject(BookRepository) private readonly bookRepository: BookRepository;
// PRIVATE routines
private async emitOrderedBookData(req: Request) {
await this.srv.emit('OrderedBook', { book: 'dada', quantity: 3, buyer: req.user.id });
}
private notifySingleInstance(req: Request, singleInstance: boolean) {
if (singleInstance) {
req.notify('Single instance');
} else {
req.notify('Entity set');
}
}
private enrichTitle(results: Book[]) {
results.map((book) => (book.title += ` -- 10 % discount!`));
}
// PUBLIC routines
public async manageAfterReadMethods(args: { req: Request; results: Book[]; singleInstance: boolean }) {
await this.emitOrderedBookData(args.req);
this.notifySingleInstance(args.req, args.singleInstance);
this.enrichTitle(args.results);
}
public notifyItemDeleted(req: Request, deleted: boolean) {
req.notify(`Item deleted : ${deleted}`);
}
public showConsoleLog() {
console.log('****************** Before read event');
}
public validateData(result: Book, req: Request) {
if (result.currency_code === '') {
return req.reject(400, 'Currency code is mandatory!');
}
}
public async addDefaultTitleText(result: Book, req: TypedRequest<Book>) {
await this.bookRepository.update({ ID: req.data.ID }, { title: 'Dracula' });
}
public async verifyStock(req: ActionRequest<typeof submitOrder>) {
const { book, quantity } = req.data;
const bookFound = await this.bookRepository.findOne({ ID: book! });
if (quantity != null) {
if (quantity < 1) {
return req.reject(400, `quantity has to be 1 or more`);
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!bookFound) {
return req.error(404, `Book #${book} doesn't exist`);
}
if (bookFound.stock !== null && quantity > bookFound.stock!) {
return req.reject(409, `${quantity} exceeds stock for book #${book}`);
}
await this.bookRepository.update(bookFound, {
stock: (bookFound.stock! -= quantity),
});
}
await this.srv.emit('OrderedBook', { book, quantity, buyer: req.user.id });
return { stock: bookFound!.stock };
}
}
export default BookService;
This BaseRepository will provide out-of-the-box functionalities like:
@Repository()
class BookRepository extends BaseRepository<Book> {
constructor() {
super(Book);
}
// ... define custom CDS-QL actions if BaseRepository ones are not satisfying your needs !
}
export default BookRepository;
In the @Repository() we've utilizied the BaseRepository by extending the BookRepository class with CDS-TS-Repository will give us access to the most common CDS-QL actions.
You can find additional documentation for BaseRepository at CDS-TS-Repository GitHub
In conclusion, CDS-TS-Dispatcher combined with CDS-TS-Repository is a powerful tool that can speed up your SAP CAP TypeScript projects by eliminating repetitive code and being a better fit for common team architecture setups.
Find an example of usage of the CDS-TS-Samples GitHub
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
10 | |
9 | |
8 | |
6 | |
6 | |
6 | |
5 | |
5 | |
5 |