Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
Dragolea
Participant

Hi all,

Introduction

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.

-

Blog series

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 bABS & DxFrontier team

-

Prerequisites

Before we dive in, you should have a basic understanding of CDS-TS-Dispatcher: Simplifying SAP CAP TypeScript Development 

 

Section 1: Introduction to Controller - Service - Repository - design pattern

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 - Responsible for managing the REST interface to the business logic implemented in Service
  • Service - Contains business logic implementations, the middleware between Controller and Repository.
  • Repository - This component is dedicated to handling entity manipulation operations by leveraging the power of CDS-QL.

Controller Service Repository design patternController Service Repository design pattern

 

Section 2: Introduction to Controller (@EntityHandler) layer

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

 

 

Section 3: Introduction to Service (@Servicelogic) layer

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;

 

 

Section 4: Introduction to Repository (@Repository) layer

This BaseRepository will provide out-of-the-box functionalities like: 

  • .create(): Create new records in the table.
  • .update(): Updates a new record in the table.
  • .createMany(): Creates many records in the table.
  • .findAll(): Retrieve all records from the table.
  • .find(): Query the database to find specific data.
  • .delete(): Remove records from the database.
  • .exists(): Check the existence of data in the table.
  • ... and many more actions

 

 

@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

 

Conclusion

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.

 

Additional Resources

Find an example of usage of the CDS-TS-Samples GitHub

Labels in this area