CDS-TyperでTypeScriptを使用したCAP開発をする

## はじめに
CDS-Typerは、CAPのデータモデルを参照したTypeScript用の型定義ファイル(.ts)を生成するツールです。コーディング時に型を使用したオートコンプリートができるようになります。

使い方は以下のドキュメントに書かれていますが、やってみてつまづいた点があったので、解決方法を含めて記事に残します。

https://cap.cloud.sap/docs/tools/cds-typer

ソースコード：https://github.com/miyasuta/cap-cds-typer

## 前提
- 開発環境はSAP Business Application Studio
- TypeScriptを使用するため、以下のパッケージをグローバルにインストール

```
npm i -g typescript ts-node
```
- VS CodeまたはBASの設定で、CDS > Type Generatorが有効になっていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e50ca1bc-9970-2f2a-90ae-ab6b5b8b07a7.png)



## ステップ
1. CAPのプロジェクトを作成
1. .tsconfigファイルを作成
1. CDS-Typerを追加
1. .cdsファイルを作成
1. サービスを実行
1. イベントハンドラを実装
1. Cloud Foundryにデプロイ

参考：[CDS-Typer | Capire](https://cap.cloud.sap/docs/tools/cds-typer#integrate-into-typescript-projects)

### 1. CAPのプロジェクトを作成
以下のコマンドでCAPのプロジェクトを作成します。

```
cds init <プロジェクト名>
```
### 2. tsconfig.jsonファイルを作成
Type Scriptを使用するため、プロジェクトのルートにtsconfig.jsonファイルを作成します。

```json
{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "outDir": "./gen/srv/srv",
        "rootDir": ".",
        "baseUrl": "./",
        "moduleResolution": "node",
        "skipLibCheck": true,
        "preserveConstEnums": true,
        "sourceMap": false,
        "allowJs": true,
        "strict": true,
        "strictNullChecks": false,
        "strictPropertyInitialization": false,
        "esModuleInterop": true,
        "paths": {
            "#cds-models/*": [
                "./@cds-models/*/index.ts"
            ]
        }
    },
    "include": [
        "./srv/**/*"
    ]
}
```
:::note warn
[参考にしたプロジェクト](https://github.com/SAP-samples/btp-full-stack-typescript-app/blob/main/tsconfig.json)では`"rootDir": "./srv",`となっていましたが、CDS-Typerはプロジェクトのルートに型定義ファイルを生成するので、そこも対象に含めるよう`"rootDir": ".",`とする必要があります。

参考：[Integrate Into TypeScript Projects](https://cap.cloud.sap/docs/tools/cds-typer#integrate-into-typescript-projects)
:::

### 3. CDS-Typerを追加
以下のコマンドでCDS-Typerを追加します。

```
cds add typer
```
これにより、package.jsonに[@cap-js/cds-typer](https://github.com/cap-js/cds-typer)が追加されます。

```json
  "devDependencies": {
    "@cap-js/sqlite": "^1",
    "@cap-js/cds-typer": ">=0.1"
  },
```

`npm i`コマンドで追加したDependencyをインストールします。

### 4. .cdsファイルを作成
以下のコマンドで、サンプルのデータモデルを作成します。
```
cds add sample-tiny
```
.cdsファイルが作成されると同時に、プロジェクトのルートに`@cds-models`というフォルダができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cd43aecd-73d7-5c82-86cd-5e3c41f3e875.png)

`@cds-models`フォルダの中に、dbフォルダやsrvフォルダで定義したエンティティの型を定義した.tsファイルが作成されています。.cdsファイル側を変更すると、これらのファイルに自動的に反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c06b80c1-c90a-b2ed-314f-821bb95d937f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/16750c74-b305-6501-9aae-35aa7cd8931b.png)

:::note warn
`@cds-models`フォルダが自動で生成されない場合は、以下のコマンドを実行します。
```
npx @cap-js/cds-typer '*' --outputDirectory @cds-models
```
参考：[CDS Typer: @cds-models folder not generated](https://answers.sap.com/questions/13971192/cds-typer-1.html)

※[2025年3月のリリース](https://cap.cloud.sap/docs/releases/mar25#initial-type-generation-with-cds-watch)で、`cds watch`実行時に自動的に`@cds-models`フォルダが生成されるようになりました
:::

:::note warn
`@cds-models`フォルダは.gitignoreに追加されるので、この状態ではGitリポジトリに格納されません。リポジトリからクローンしたプロジェクトには含まれていないことに注意が必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/08ff64b5-ba57-1730-92a7-2b05a3cbf4d7.png)
:::

データモデルに少し手を加えて、以下のようにします。

```db/data-model.cds
namespace my.bookshop;

entity Books {
  key ID : Integer @title: 'Book ID';
  title  : String @title: 'Title';
  stock  : Integer @title: 'Stock';
  price: Integer @title: 'Price'; //追加
}

//追加
entity Orders {
  key ID: UUID @title: 'Order ID';
  book: Association to Books @title: 'Book';
  quantity: Integer @title: 'Quantity';
  amount: Integer @title: 'Amount';
}
```

```srv/cat-service.cds
using my.bookshop as my from '../db/data-model';

service CatalogService {
    @readonly entity Books as projection on my.Books;
    @odata.draft.enabled
    entity Orders as projection on my.Orders;
}

annotate CatalogService.Orders with @(
    UI: {
        LineItem  : [
            {
                $Type : 'UI.DataField',
                Value : ID,
            },
            {
                $Type : 'UI.DataField',
                Value : book_ID,
            },
            {
                $Type : 'UI.DataField',
                Value : quantity,
            },
            {
                $Type : 'UI.DataField',
                Value : amount,
            }
        ],
        HeaderInfo  : {
            $Type : 'UI.HeaderInfoType',
            TypeName : 'Order',
            TypeNamePlural : 'Orders',
        },
        Identification  : [
            {
                $Type : 'UI.DataField',
                Value : book_ID,
            },
            {
                $Type : 'UI.DataField',
                Value : quantity,
            },
            {
                $Type : 'UI.DataField',
                Value : amount,
            }            
        ],
        Facets  : [
            {
                $Type : 'UI.ReferenceFacet',
                Target : '@UI.Identification',
            },
        ],
    }

){
    amount @Common : { FieldControl : #ReadOnly, }
};
```

### 5. サービスを実行
以下のコマンドでサービスを実行します。
```
cds-ts watch
```
:::note
通常、`cds watch`コマンドを使いますが、TypeScriptの場合は`cds-ts watch`を使用します。
:::

### 6. イベントハンドラを実装
TypeScriptでイベントハンドラを実装します。srvフォルダの中に`cat-service.ts`ファイルを作成し、以下のコードを実装します。Order登録時に本の価格をDBから取得し、数量と掛け合わせて金額(amount)を算出します。

```typescript
import cds from '@sap/cds';

module.exports = class CatalogService extends cds.ApplicationService { async init() {
    const { Book, Books, Order } = await import('#cds-models/CatalogService')

    this.before('CREATE', Order, async(req) => {
        console.log('Create Order called')
        //calculate amount
        const { price } = await SELECT .one (Books, b => {b.price}) .where `ID = ${req.data.book_ID}`
        console.log('price: ', price)
        req.data.amount = req.data.quantity * price
    })

    return super.init()
}}
```
:::note warn
以下のワーニングが出たら、指示通りに`npm i --save-dev @types/node`を実行します。その後、一旦ファイルを閉じて開くとエラーが解消します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/377681fb-443e-bbd4-7ee0-3b09fcbf33e1.png)
:::

:::note warn
[ドキュメント](https://cap.cloud.sap/docs/tools/cds-typer#cql)では、`require('#cds-models/...')`により型定義をインポートしていますが、この方法だとオートコンプリートが利かないので`await import`を使用する必要があります。
参考：[Code completion with CDS Typer does not work](https://answers.sap.com/questions/14019787/code-completion-with-cds-typer-does-not-work.html)
:::

これにより、使用可能なプロパティやメソッドが提案されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d230e409-68b6-30aa-b128-baf554b70ddb.png)

存在しないプロパティを指定した場合はエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2ad77207-b679-f32f-9165-1f609a93c5f2.png)

OrdersのFiori Previewから動作を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/09a29260-37e6-f751-ef65-fc6d8a9f6ef4.png)

Orderを登録すると、Amountが自動計算されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d1e3aa7e-6831-3f71-9799-aefd2def797d.png)

### 7. Cloud Foundryにデプロイ
TypeScriptで作成したCAPプロジェクトのビルド、デプロイ方法については過去のブログを参考にします。

https://qiita.com/tami/items/ed4f3fa2ccf27f4fd3e5#6-%E3%83%93%E3%83%AB%E3%83%89%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4

#### 7.1 ビルド用スクリプトを追加

- package.jsonに以下のスクリプトを追加
```package.json
  "scripts": {
    ...
    "build:cf": "npm run build:cds && npm run cleanup:ts && npm run build:ts",
    "build:ts": "tsc",
    "build:cds": "cds build --production",
    "cleanup:ts": "npx rimraf gen/srv/srv/*.ts"
  }
  ```
※rimrafを使うため`npm i rimraf --save-dev`でインストールしておく必要があります。

#### 7.2 Cloud Foundry環境で必要な設定を追加
```
cds add hana --for production
cds add xsuaa --for production
cds add approuter --for production
```
xs-security.jsonファイルにredirect-urisを追加します。
```json
{
  "scopes": [],
  "attributes": [],
  "role-templates": [],
  "oauth2-configuration": {
    "redirect-uris": [
      "https://*.hana.ondemand.com/login/callback"
    ]
  }
}
```
これがないと、ApprouterのURLを開いたときに以下のエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/aee1a7c2-f3da-0772-f48f-fd0b834579b3.png)

#### 7.3 mta.yamlを生成
```
cds add mta
```

mta.yamlのビルドコマンドを以下のように変更します。
```yaml
build-parameters:
  before-all:
    - builder: custom
      commands:
        - npm install
        - npm run build:cf
```

#### 7.4 ビルド
```
mbt build -t gen --mtar mta.tar
```
genフォルダを見ると、.jsに変換されたイベントハンドラのファイルができています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2ca90256-4a6d-a2b8-971a-07e31730a372.png)

#### 7.5 デプロイ
```
cf deploy gen/mta.tar
```
ターミナルに表示されたApprouterのURLから入って確認します。
```
Application "cds-typer" started and available at "xxx-dev-cds-typer.cfapps.us10-001.hana.ondemand.com"
```
最初は（index.htmlファイルがないため）Not Foundとなりますが、`/odata/v4/catalog/$metadata`と入力するとサービスにアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7b0d7971-4f62-9c90-6362-4114dde033f9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/23c1f868-6a95-6d24-e394-e40ff5c4f073.png)

## おわりに
この記事では、CDS-Typer使ってTypeScriptを使用したCAPの開発をしました。次回はここにTypeScriptで作るUI5アプリを追加したいと思います。