First project - SAP CAP Guide 2
After we have prepared our environment it is time to dive in and get started with coding. If you have not read it, I would recommend reading it first. Click here to get to it.
Create new project in Visual Studio Code
Now we can finally create a new project in Visual Studio Code. To do that click on “Terminal” - “New Terminal” in the navigation bar. Navigate in a directory where you want to to create your new project folder using cd. To create a new project type:
cds init myproject
Now navigate into the directory using cd myproject
. The cds package offers a really cool functionality that let’s you start the cds service and it will always restart whenever you changed something like nodemon - which it essentially is using it:
cds watch
Defining the model
After we are created all the necessary files and got cds watch
up and running, we need to setup our domain model. This model does contain our schema which holds all our data and entity definitions. We describe how an entity should look like and what data can be filled in. Applying the managed
feature on an entity allows us to have additional information like createdAt, createdBy, modifiedAt, modifiedBy in our entity. The fields will be populated whenever a user changes or creates an entry in your table. As every entity needs a key, where an entry can be identified with, we will use an Integer for the Books and Author. Orders and OrderItems which are not predefined by us, will be assigned a random unique ID by the UUID function.
The models of the database are contained in the db/ directory and the file name always ends with .cds. So we are now creating the file schema.cds
in the /db directory:
namespace timoschuetz.demo.bookshop;
using { Currency, managed } from '@sap/cds/common';
entity Books: managed {
key ID: Integer;
title: localized String(111);
descr: localized String(1111);
author: Association to Authors;
stock: Integer;
price: Decimal(9, 2);
currency: Currency;
}
entity Authors: managed {
key ID: Integer;
name: String(111);
books: Association to many Books on books.author = $self;
}
entity Orders: managed {
key ID: UUID;
OrderNo: String @title:'Order Number';
Items: Composition of many OrderItems on Items.parent = $self;
}
entity OrderItems {
key ID: UUID;
parent: Association to Orders;
book: Association to Books;
amount: Integer;
}
Create services
Our data model is basically useless without a service to access it. A service is able to give users access to the data which is stored in the database, but also has the option to restrict or permit user access to certain services. Furthermore the type of Operation like READ and WRITE can be restricted. With that the same service can be used by a user, e.g. for reading which books are available, and an admin, who can read and insert new books, the same time. This does help us to keep the amount of services as low as possible, because more services, result in more work and therefore in more costs. We will get to how this exactly works later on in this series.
Services as you might already found out are defined in the /srv folder. Here we create a new file called browse-service.cds
which will hold our first service:
using { timoschuetz.demo.bookshop as my } from '../db/schema';
service ExploreService @(path:'/explore') {
entity Books as select from my.Books {
key ID, title, author, stock
};
entity Authors as select from my.Authors {
key ID, name, books
};
entity Orders as projection on my.Orders ;
}
Ok, but what does the entities tell us? First of all, we have two options to expose a entity using a service: the select function and the projection
With a select as the name indicates we select which data we want expose from our entity. In this case for the Books entity it is the ID, which is a key, the title and the author. With a projection we expose everything of the service and have the option to exclude data using exclude. We will get to that later too. The path is another parameter which might be interesting for you as it makes it more easy to navigate the services. Giving them a proper name definitely helps.
Now our first service is ready to be tested. Navigate to localhost:4004/explore
and take a look at the service:
When you click on one service you can see that there is no data exposed, as we have no data inside our database right. Actually we do not have a persistent database yet, we are running an in-memory database which can be prefilled with csv files. This is what we are going to do. Create a new folder in the /db directory called data. In this folder we will place our csv files, which contain some demo data:
timoschuetz.demo.bookshop-Authors.csv
ID;name
101;Emily Brontë
107;Charlotte Brontë
150;Edgar Allen Poe
170;Richard Carpenter
timoschuetz.demo.bookshop-Books.csv
ID;title;author_ID;stock
201;Wuthering Heights;101;12
207;Jane Eyre;107;11
251;The Raven;150;333
252;Eleonora;150;555
271;Catweazle;170;22
Custom Logic
As of now our application was just used for exposing data from a database, not very special. Now we get to the better part: Custom Logic!
With just one file we can add custom logic to our service without much of a hassle. Create a new file with the same name as the service, but instead of a cds file, create a javascript file:
browse-service.js
module.exports = (srv)=>{
const {Books} = cds.entities
// Add some discount for overstocked books
srv.after ('READ','Books', (each)=>{
if (each.stock > 111) each.title += ' -- 11% discount!'
})
// Reduce stock of books upon incoming orders
srv.before ('CREATE','Orders', async (req)=>{
const tx = cds.transaction(req), order = req.data;
if (order.Items) {
const affectedRows = await tx.run(order.Items.map(item =>
UPDATE(Books) .where({ID:item.book_ID})
.and(`stock >=`, item.amount)
.set(`stock -=`, item.amount)
)
)
if (affectedRows.some(row => !row)) req.error(409, 'Sold out, sorry')
}
})
}
If you create a javascript file with the same name as the service, it will be laid on top of it. Your can decide if you want the logic getting called before or after the service has been called. Another great feature is the option to specify what data will be called, instead of which service. I really like this feature as it not only affects one specific service, but all other services that are linked to it. Like books is linked to the authors and wise versa. When calling a service we have the option to pull the linked information as well. E.g. When we try to pull the information about one or more authors, we can „extend“ the books property to pull more information about the books which are written by the authors we pulled. Try it out and pull all authors and extend the books property: http://localhost:4004/explore/Authors?$expand=books($select=ID,title)
As you can see we defined the custom to whenever Books are read from the database and it extends the title to tell us, that this book will be discounted, because our stock is very large. The other logic will step in before we write our order to the database and checks if we actually have enough books in stock to serve this order and reduces the stock, so that we do not „sell“ books we do not have in stock. All of this happens before the order will be created to prevent selling more products than you actually have, makes sense right?