Skip to content

Latest commit

 

History

History
612 lines (417 loc) · 21 KB

README.md

File metadata and controls

612 lines (417 loc) · 21 KB

Exercise 1 - Introduction to CAP

In this exercise, you will build a small application with SAP Cloud Application Programming Model (CAP).

You will use this application scenario throughout the exercises. Also, you will get familiar with CAP and the CDS language.

The conceptual domain model for this Incidents Management application is as follows:

  • Customers can create Incidents (either directly or via agents)
  • Incidents have a title, a status and and urgency level
  • Incidents contain a Conversation history consisting of several messages

Domain model

Create a Project

👉 In SAP Business Application Studio, create a new CAP Project through the project wizard.

  • Name it incidents-mgt, for example.
  • Accept the rest of the defaults. No sample code needed; you will fill the project as you go.
These screenshots help you find the project wizard:

New CAP Project

New CAP Project - Details

You might also create the project with cds init incidents-mgt on the command line in the /home/user/projects folder.

Add Incidents

You should now have (been) switched to a new workspace with the created project.

👉 Open the file explorer again.

👉 Create a file data-model.cds in the db folder.

  • There, add an Incidents entity with a key field ID and a title.
  • Choose appropriate data types. Use code completion (intellisense) to pick a fitting data type.
  • Also, add a namespace incidents.mgt to the beginning of the file, so that the entity's full name is incidents.mgt.Incidents
This is how it should like:
namespace incidents.mgt;

entity Incidents {
  key ID       : UUID;
  title        : String;
}

Use Predefined Aspects

The situation of ID key fields is so common that there is a prebuilt CDS aspect available named cuid that provides just that.
It can be imported with using ... from '@sap/cds/common'; and used in an entity with the : (colon) syntax.

Also, the Incidents entity shall carry information on when it was created and updated and by whom. There is a managed aspect from @sap/cds/common that does that.

👉 Make use of the two aspects and:

  • Replace the hand-crafted ID field with cuid
  • Add the managed aspect.
This is how it should like:
using { cuid, managed } from '@sap/cds/common';

namespace incidents.mgt;

entity Incidents : cuid, managed {
  title        : String;
}

👉 Take a few moments and check out what the @sap/cds/common package has to offer in addition. In the editor, hold Ctrl (or ) and hover over the managed text. Click to navigate inside. See the documentation for more.

Add a Conversation History

An incident shall hold a number of messages to build a conversation history.

To create such a relationship, the graphical CDS modeler in SAP Business Application Studio is a great tool.
👉 Open it for the data-model.cds file using one of two options:

  • Right click the data-model.cds file. Select Open With > CDS Graphical Modeler
  • Or open the modeler through the project Storyboard:
    • Press F1 > Open Storyboard
    • Click on the Incidents entity > Open in Graphical Modeler

👉 In its canvas, add a Conversations entity.

  • In the Aspects tab in the property sheet, add the ID key field from CDS aspect cuid.
  • Add timestamp, author, and message fields with appropriate types.

👉 Now connect the two entities.

  • Hover over the Incidents entity and find the Add Relationship button from the flyout menu. Drag it from Incidents to the Conversations entity.
  • In the New Relationship dialog:
    • Choose a relationship type so that whenever an Incident instance is deleted, all its conversations are deleted as well.
    • Stay with the proposed conversations and incidents fields.
All in all, the entities shall look like this:

Incidents and Conversations entities in graphical modeler

As text, it looks like this. Note the Composition between the two entities.

using { cuid, managed } from '@sap/cds/common';

namespace incidents.mgt;

entity Incidents : cuid, managed {
  title         : String(100);
  conversations : Composition of many Conversations on conversations.incidents = $self;
}

entity Conversations : cuid, managed {
  timestamp : DateTime;
  author    : String(100);
  message   : String;
  incidents : Association to Incidents;
}

To open the code editor, just double-click on the db/data-model.cds file in the explorer tree.

Add Status and Urgency

Incidents shall have two more fields status and urgency, which are 'code lists', i.e. configuration data.

👉 Add two entities, using the sap.common.CodeList aspect.

  • Status for the incident's status like new, in process etc.
    • Name its key field code instead of ID.
  • Urgency to denote the priority like high, medium etc.
    • Name its key field code instead of ID.

👉 Add one association to Incidents pointing to the each new entity. The associations shall be unidirectional only, i.e. pointing from Incidents to Status or Urgency, but not in the other direction.

See the result:

In db/data-model.cds, add:

using { sap.common.CodeList } from '@sap/cds/common';

entity Status : CodeList {
  key code  : String;
}

entity Urgency : CodeList {
  key code : String;
}

entity Incidents {
  ...
  urgency       : Association to Urgency;
  status        : Association to Status;
};

Create a CDS Service

There shall be an API for incidents processors to maintain incidents.

👉 In a new file srv/processor-service.cds, create a CDS service that exposes a one-to-one projection on Incidents.

This is how the service should like:
using { incidents.mgt } from '../db/data-model';

service ProcessorService {

  entity Incidents as projection on mgt.Incidents;

}

Start the Application

👉 Run the application:

  • Open a terminal. Press F1, type new terminal, or use the main menu.

  • In the terminal, execute in the project root folder:

    cds watch
    See the console output:

    Start application, terminal output

Take a moment and check the output for what is going on:

  • The application consists of three cds files. Two are application sources and one comes from the @sap/cds library:

    [cds] - loaded model from 3 file(s):
    
      srv/processor-service.cds
      db/data-model.cds
      .../@sap/cds/common.cds
  • An in-memory SQLite database got created. This holds the application data (which we don't have yet).

    [cds] - connect to db > sqlite { database: ':memory:' }
    /> successfully deployed to in-memory database.
  • The CDS service got exposed on this path:

    [cds] - serving ProcessorService { path: '/odata/v4/processor' }

👉 Now Ctrl+Click on the http://localhost:4004 link in the terminal.

  • In SAP Business Application Studio, this URL gets automatically transformed to an address like https://port4004-workspaces-ws-...applicationstudio.cloud.sap/
  • If you work locally, this would be http://localhost:4004.

On the index page, all endpoints are listed along with the entities that they expose.

Index page with list of endpoints and entities

The Fiori preview link you will use later.

👉 Do you know why the service URL path is /processor? What's the $metadata link?

Here is why:

You named the CDS service ProcessorService, and the runtime system infers the URL processor by stripping off Service. You can configure this explicitly using the @path annotation.

The $metadata URL serves the metadata document required for the OData protocol. You will soon see OData in action.

Add Sample Data

Add some test data to work with.

👉 Create empty csv files for all entities. In a new terminal, run:

cds add data

As soon as they are there, cds watch finds and deploys them to the database. Check the console output:

[cds] - connect to db > sqlite { database: ':memory:' }
> init from db/data/incidents.mgt-Urgency.texts.csv
> init from db/data/incidents.mgt-Urgency.csv
> init from db/data/incidents.mgt-Status.texts.csv
> init from db/data/incidents.mgt-Status.csv
> init from db/data/incidents.mgt-Incidents.csv
> init from db/data/incidents.mgt-Conversations.csv

Note how the files names match the entity names.

Now fill in some content:

👉 For the two code lists, add csv records in the terminal real quick:

cat << EOF > db/data/incidents.mgt-Status.csv
code,name
N,New
I,In Process
C,Closed
EOF

cat << EOF > db/data/incidents.mgt-Urgency.csv
code,name
H,High
M,Medium
L,Low
EOF

👉 For the Incidents and Conversations csv files, use the sample data editor to fill in some data.

  • Double click on the db/data/incidents.mgt-Incidents.csv file in the explorer tree.
  • In the editor, add maybe 10 rows. Use the Number of rows field and click Add to create the records.
  • Also create records for the db/data/incidents.mgt-Conversations file. The editor automatically fills the incidents_ID foreign key.

👉 On the applications index page, click on the Incidents link which runs a GET /odata/v4/processor/Incidents request.

Add a Simple UI

👉 Click on Incidents > Fiori Preview on the index page of the application. This opens an SAP Fiori Elements application that was created on the fly. It displays the entity's data in a list.

The list seems to be empty although there is data available . This is because no columns are configured. Let's change that.

👉 Add a file app/annotations.cds with this content:

using { ProcessorService as service } from '../srv/processor-service';

// enable drafts for editing in the UI
annotate service.Incidents with @odata.draft.enabled;

// table columns in the list
annotate service.Incidents with @UI : {
  LineItem  : [
    { $Type : 'UI.DataField', Value : title},
    { $Type : 'UI.DataField', Value : modifiedAt },
    { $Type : 'UI.DataField', Value : status.name, Label: 'Status' },
    { $Type : 'UI.DataField', Value : urgency.name, Label: 'Urgency' },
  ],
};

// title in object page
annotate service.Incidents with @(
    UI.HeaderInfo : {
      Title : {
        $Type : 'UI.DataField',
        Value : title,
      },
      TypeName : 'Incident',
      TypeNamePlural : 'Incidents',
      TypeImageUrl : 'sap-icon://alert',
    }
);

which creates 3 columns:

Fiori list page with 3 columns

There is even a preconfigured label for the modifiedAt column.
👉 Do you know how to look them up? Hint: use editor features.

See how:

On the managed aspect in db/data-model.cds, select Go to References from the context menu. Expand common.cds in the right-hand tree and check the annotate managed entries until you see the @title annotations:

Dialog with all references of the managed aspect

The actual strings seem to be fetched from a resource bundle that is addressed with a {i18n>...} key. See the localization guide for how this works.

The label for the title column seems to be wrong, though.
👉 Fix it by adding the appropriate CDS annotation to the Incidents.title element.

This is how you can do it:

Add a @title:'Title' annotation to the Incidents definition. Make sure to place it correctly before the semicolon. Watch out for syntax errors.

entity Incidents : cuid, managed {
  title         : String(100) @title : 'Title';   // <--
  ...
}

Note that annotations can be added at different places in the CDS syntax.

Add Business Logic

Let's add some logic to the application. When an incident is created with urgent in its title, it shall set its urgency to 'High'.

👉 Add a file srv/processor-service.js with this content:

const cds = require('@sap/cds')

class ProcessorService extends cds.ApplicationService {
  async init() {

    this.before('CREATE', 'Incidents', ({ data }) => {
      if (data) {
        const incidents = Array.isArray(data) ? data : [data]
        incidents.forEach(incident => {
          // TODO add code here
        })
      }
    })

    return super.init()
  }
}

module.exports = ProcessorService

Note how the js file is named the same as the cds file. This is how the framework finds the implementation. You can can see this in the output of cds watch, where it prints the impl value:

...
[cds] - serving ProcessorService { path: '/odata/v4/processor', impl: 'srv/processor-service.js' }
...

Don't see the js file listed there? Check its spelling!

👉 Complete the code with the actual logic: check that the title includes urgent and in that case set its urgency code to H.

  • Handle urgent and Urgent in the same way.
  • Also be robust in the case that there is no title given.
Solution:
          if (incident.title?.toLowerCase().includes('urgent')) {
            incident.urgency = { code: 'H' }
          }

👉 Now test the logic by creating an incident through the UI. Add the word urgent in the title. After saving it, go back to the list. You should see the urgency set to High.

Debug the Code (Optional)

If you want to debug the code using the built-in visual Javascript debugger, do this:

  • Kill the running cds watch process.

  • Press F1, type debug terminal, select Javascript: Debug Terminal

  • In this terminal, start cds watch as usual. The debugger starts and attaches to this process.

  • At the top in the middle of the window, see the floating panel with which you can control the debugger and do step operations.
    Debugger controls

  • Set a breakpoint in the source within the this.before(... function. Do this by double-clicking next to the line number.

    Quick question: in this situation, why wouldn't the debugger halt outside of this function?

    Because the before() function is a request handler, and it's only such request-handling code that can be debugged now.
    The code above and below is bootstrap code that can only be debugged if you either set the breakpoint earlier or make the debugger halt right when the server process gets started.

  • Now create a new incident. The UI freezes because the debugger has stopped.

  • For variables, press F1, type variables, select Run and Debug: Focus on Variables View.

  • After having inspected the variables, don't forget to continue execution using the debug control panel, otherwise the application UI will not react (and timeout eventually).

Add Another Service

In the service above, you have used only the very minimal form of a CDS projection, which basically does a one-to-one exposure of an entity to the API surface:

service ProcessorService {
  entity Incidents as projection on mgt.Incidents;
}

However, projections go way beyond this and provide powerful means to express queries for specific application scenarios.

  • When mapped to relational databases, such projections are in fact translated to SQL views.
  • You will soon see non-DB uses of projections.

👉 Now explore projections and services. Add a 'statistics service' that shows

  • Incidents' title
  • Their status, but showing New instead of N etc. Hint: use a path expression for the name.
  • Only urgent incidents. Hint: use a where condition.

The result shall be available at /odata/v4/statistics/UrgentIncidents. What's the name of the CDS service that matches to this URL?

Also, use the editor's code completion that guides you along the syntax.

Solution:

In a separate srv/statistics-service.cds file, add this:

using { incidents.mgt } from '../db/data-model';

service StatisticsService {

  entity UrgentIncidents as projection on mgt.Incidents {
    title,                  // expose as-is
    status.name as status,  // expose with alias name using a path expression
  }
  where urgency.code = 'H'  // filter
}

👉 If you got this, add these fields with more advanced syntax:

  • modified : a concatenated string from modifiedAt and modifiedBy (use the str1 || str2 syntax)
  • conversationCount : a count for the number of conversation messages. Hint: SQL has a count() function. Don't forget the group by clause.
Solution:
using { incidents.mgt } from '../db/data-model';

service StatisticsService {

  entity UrgentIncidents as projection on mgt.Incidents {
    title,                  // expose as-is
    status.name as status,  // expose with alias name using a path expression

    modifiedAt || ' (' || modifiedBy || ')' as modified          : String,
    count(conversations.ID)                 as conversationCount : Integer
  }
  where urgency.code = 'H' // filter
  group by ID              // needed for count()
}

Check on /odata/v4/statistics/UrgentIncidents for the results. Note that they will vary depending on your sample data.

Remember: you got all of this power without a single line of (Javascript or Java) code!

Test OData Features (Optional)

Let's inspect some of the built-in features of OData.

👉 In the browser, append to the service URL .../odata/v4/processor/Incidents so that you can:

  • list incidents
  • with their conversation messages,
  • limiting the list to 5 entries,
  • only showing the title field,
  • sorting alphabetically along title

How can you do that using OData's query options like $expand etc.?

This is how:

Add

?$select=title&$orderby=title&$top=5&$expand=conversations

to the URL.

Inspect the Database (Optional)

Upon deployment to the database, CAP creates SQL DDL statements to create the tables and views for your entities.

👉 On the db/data-model.cds file, select CDS Preview > Preview as sql from the editor's context menu. This opens a side panel with the SQL statements.

See how this looks like:

SQL preview for data model

👉 You can do the same in the terminal with

cds compile db --to sql

👉 Now do the same on file srv/statistics-service.cds. What is different in the result? Can you explain where the new SQL statements come from?

This is why:

For each CDS projection, an SQL view is created that captures the queries from the projections. This is why you see a lot more CREATE VIEW statements.

Summary

You've now created a basic version of the Incidents Management Application. Still it's very powerful as it:

  • Exposes rich API's and OData metadata. You will see OData clients like SAP Fiori Elements UI soon.
  • Deploys to a database out-of-the-box, incl. data files.
  • Let's you stay focused on the domain model without the need to write imperative code for simple CRUD requests.
  • Keeps boilerplate files to the minimum. Just count the few files in the project.

Now continue to exercise 2, where you will extend the application with remote capabilities.