CAPでTypeScriptを使ってみる (cds2types)

## はじめに
SAP Cloud Application Programming Model (CAP)は[TypeScriptの使用をサポートしています](https://cap.cloud.sap/docs/node.js/typescript)。CAPでTypeScriptを使用する方法については、以下のブログおよびGitリポジトリが参考になります。

**①Typeの定義を自分で行う方法**
- ブログ | [Full Stack TypeScript App for Cloud Foundry – Sample Repository](https://blogs.sap.com/2021/12/09/full-stack-typescript-app-for-cloud-foundry-sample-repository/)
- Gitリポジトリ | [SAP-samples/btp-full-stack-typescript-app](https://github.com/SAP-samples/btp-full-stack-typescript-app)

**②Typeの定義をcds2tpesにより行う方法**
[cds2types](https://www.npmjs.com/package/cds2types)はcdsで定義したスキーマやサービスをもとに、自動的にTypeの定義を生成してくれるツールです。

- ブログ | [Taking CAP to the next level – with TypeScript](https://blogs.sap.com/2020/05/22/taking-cap-to-the-next-level-with-typescript/)
- Gitリポジトリ | [HeneryHawk/cap-bookshop-typescript](https://github.com/HeneryHawk/cap-bookshop-typescript)

## やってみること
このブログでは以下のことを行います。
- [SAP-samples/cloud-cap-samples/bookshop](https://github.com/SAP-samples/cloud-cap-samples/tree/main/bookshop)で定義されているスキーマおよびサービスをもとに、②の方法で型の定義を生成
- イベントハンドラをTypeScriptで実装
- 作成したプロジェクトのビルド&デプロイを実施

## 環境について
始めはローカルのVS Codeを使用していましたが、TypeScriptのCAPサービスを起動するための`cds-ts`コマンドがWindowsでは動かなかったため、途中からBusiness Application Studioに切り替えました。

## ステップ
1. 事前準備
1. ベースとなるプロジェクトを作成
1. tsconfig.jsonを追加
1. cds2typesでinterfaceとenumを生成
1. イベントハンドラをtsで作成
1. ビルド、デプロイ

### 1. 事前準備
[capire](https://cap.cloud.sap/docs/node.js/typescript#enable-typescript-support)の指示に従い、typescriptとts-nodeをインストールします。
```
 npm i -g typescript ts-node
```
### 2. ベースとなるプロジェクトを作成
以下のコマンドで通常通りCAPプロジェクトを作成します。

```
cds init bookshop-ts
```
[参考](https://github.com/SAP-samples/cloud-cap-samples/tree/main/bookshop)のリポジトリからcdsおよびcsvファイルをコピーして作成します。最終的に以下のような形になります。元のリポジトリにはサービスが複数ありますが、ここではcat-service.cdsのみ使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/eda0cbda-af2a-5f2a-7ef0-381dd58c7586.png)

上記の状態のプロジェクトをGitリポジトリのmainブランチに置いています。
https://github.com/miyasuta/bookshop-ts

### 3. tsconfig.jsonを追加
プロジェクトのルートにtsconfig.jsonファイルを追加します。設定内容は以下のGitリポジトリを参考にしました。
[SAP-samples/btp-full-stack-typescript-app](https://github.com/SAP-samples/btp-full-stack-typescript-app)

```json
{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "outDir": "./gen/srv/srv",
        "rootDir": "./srv",        
        "baseUrl": "./",
        "moduleResolution": "node",
        "skipLibCheck": true,
        "preserveConstEnums": true,
        "sourceMap": false,
        "allowJs": true,
        "strict": true,
        "strictNullChecks": false,
        "strictPropertyInitialization": false,
        "esModuleInterop": true    
    },
    "include": [
        "./srv/**/*"
    ]
}
```

### 4. cds2typesでinterfaceとenumを生成
[cds2types](https://www.npmjs.com/package/cds2types)をdevdependencyに追加します。

```
npm install --save-dev cds2types
```
cds2typesコマンドの実行でエラーになったため、`npm install -g cds2types`でcds2typesをグローバルにもインストールしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ed844367-f0cc-c9a6-19c9-34b9de1a2dc2.png)
.tsファイル格納用のディレクトリを用意します。どこに作ってもよいのですが、ここではsrv/typesとしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/432ea0cb-79e8-dbf6-7565-9cbe93fae4de.png)

以下のコマンドを実行して、.cdsファイルからインターフェースおよびenumの定義を作成します。
```
cds2types --cds srv/cat-service.cds --output srv/types/ --prefix I  
```
4つのファイルが生成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3f38c9e6-0f15-aade-b10f-65793df7e6b4.png)

- **CatalogService.ts**: サービスで定義したエンティティに対応するインターフェース、およびエンティティを列挙したenum
- **other.ts**: User, Entity, SanitizedEntityを定義（中はほぼ空）
- **sap.capire.bookshop.ts**: DBのスキーマで定義したエンティティに対応するインターフェース、およびエンティティを列挙したenum
- **sap.common.ts**: `@sap/cds/common`で提供される事前定義エンティティ(Languages, Countries, Currencies等）に対応するインターフェース、およびenum

### 5. イベントハンドラをtsで作成
ステップ4で作成したインターフェースを使用してTypeScriptでイベントハンドラを作成していきます。まずは、[こちら](https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/cat-service.js)のリポジトリにあるjsのソースをコピーしてtsとして保存してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/251380f7-68af-fbb5-7a40-1411c3fcfd77.png)
貼り付けた時点では色々なエラーが出ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d8aceb86-b9a9-2ab7-bf26-e83d0fdc3d9a.png)

以下のようにソースを変更しました。このあとTypeSceiptにするにあたって変更したポイントを説明します。

```typescript
import { ApplicationService } from '@sap/cds'
import { IListOfBooks, IBooks, IActionSubmitOrderParams } from './types/CatalogService'

export class CatalogService extends ApplicationService { async init(){
  const db = await cds.connect.to('db')
  const { Books } = db.entities
  const { ListOfBooks } = this.entities

  // Reduce stock of ordered books if available stock suffices
  this.on ('submitOrder', async req => {
    const params = req.data as IActionSubmitOrderParams
    const {book,quantity} = params
    if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
    let b = await SELECT `stock` .from (Books,book) as IBooks
    if (!b) return req.error (404,`Book #${book} doesn't exist`)
    let {stock} = b
    if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
    await UPDATE (Books,book) .with ({ stock: stock -= quantity })
    await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
    return { stock }
  })

  // Add some discount for overstocked books
  this.after ('READ', ListOfBooks, each => {
    const listOfBooks = each as IListOfBooks
    if (listOfBooks.stock > 111) listOfBooks.title += ` -- 11% discount!`
  })

  return super.init()
}}
```

#### 5.1. ソースの変更箇所
**①ApplicationServiceをインポートする**
TypeScriptでは`require`が使えないのと、`import cds from...`としても`cds.ApplicationService`という書き方ができなかったため、以下のように変えました。

```typescript:before
const cds = require('@sap/cds')

class CatalogService extends cds.ApplicationService { init(){
```
```typescript:after
 import { ApplicationService } from '@sap/cds'

 export class CatalogService extends ApplicationService { async init(){
```

**②cds.entitesの代わりにdb.entitiesを使う**
"entites"というプロパティがcds_facadeに存在しないというエラーになったため、まずdbに接続し、db.entitesによってBooksを取得してくるようにしました。

```typescript:before
  const { Books } = cds.entities ('sap.capire.bookshop')
```
```typescript:after
   const db = await cds.connect.to('db')
   const { Books } = db.entities
```

**③cds2typesで定義したインターフェースを使う**
①②でエラーは消えますが、せっかくステップ4.でインターフェースを作成したのでそれらを使ってみます。
まず先頭でインターフェースの定義をインポートします。

```typescript
import { IListOfBooks, IBooks, IActionSubmitOrderParams } from './types/CatalogService'
```
- submitOrderのイベントハンドラでは、パラメータ`req.data`を`IActionSubmitOrderParams`の型にキャストします。
```typescript:before
  this.on ('submitOrder', async req => {
    const {book,quantity} = req.data
```

```typescript:after
  this.on ('submitOrder', async req => {
    const params = req.data as IActionSubmitOrderParams
    const {book,quantity} = params
```
これにより、インターフェースに定義されていない項目を受け取ろうとするとエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/eb1008fd-4325-3d43-3dc1-688acbae088b.png)

- データベースからSELECTする箇所では、結果を`IBooks`の型にキャストします。

```typescript:before
    let b = await SELECT `stock` .from (Books,book) 
```

```typescript:after
    let b = await SELECT `stock` .from (Books,book) as IBooks
```

- READのイベントハンドラでは、パラメータ`each`を`IListOfBooks`の型にキャストします。

```typescript:before
  this.after ('READ', ListOfBooks, each => {
    if (each.stock > 111) each.title += ` -- 11% discount!`
  })
```
```typescript:after
  this.after ('READ', ListOfBooks, each => {
    const listOfBooks = each as IListOfBooks
    if (listOfBooks.stock > 111) listOfBooks.title += ` -- 11% discount!`
  })
```


#### 5.2. ローカルで実行
ローカルで実行するには、以下のコマンドを実行します。
```
cds-ts watch
```
※WindowsのPowerShellで実行すると、エラーが出ました。以下のスレッドにワークアラウンドが載っていますが、今回はBASでの実行に切り替えました。
https://answers.sap.com/questions/13560254/cap-cds-ts-powershell-bug.html
https://answers.sap.com/questions/13431720/capcds-ts-powershell-the-term-ts-nodeexe-is-not-re.html
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/163f2d6c-2976-99a2-7b95-7e2651190734.png)

### 6. ビルド、デプロイ
#### 6.1. スクリプトを追加
- [こちら](https://blogs.sap.com/2021/12/09/full-stack-typescript-app-for-cloud-foundry-sample-repository/)のブログを参考に以下のスクリプトをpackage.jsonに追加します。
```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"
  }
```
#### 6.2. HANA Cloudを使用する設定を追加

```
cds add hana
```
#### 6.3. mta.yamlを追加
```
cds add mta
```

mta.yamlのbuild-parametersのコマンドを以下のように変更します。
※参考にした[リポジトリ](https://github.com/SAP-samples/btp-full-stack-typescript-app)では`npm install --production`コマンドを使っていましたが、devDependencyとしてインストールした`@types/node`（後述）が使えなくなってしまうため、devDependencyも含めてインストールするようにしています。

```yaml
build-parameters:
  before-all:
    - builder: custom
      commands:
        - npm install
        - npm run build:cf
```
#### 6.4. ビルド

```
mbt build
```
ビルドの中で実行される`tsc`コマンドで以下のエラーが発生しました。
①Bufferが見つからない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e967bb84-ed58-96c2-2ac9-70912580ee7e.png)
対応：`npm i --save-dev @types/node`を実行

②Currenciesが見つからない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d270da9a-c336-8df8-26ec-b8a252f05f9c.png)

対応：ICurrenciesをインポートして、Currenciesの代わりにICurrenciesを使用する
```typescript:types/sap.capire.bookshop.ts
import { Locale, ICurrencies } from "./sap.common";

export interface IBooks {
    ...
    currency: ICurrencies;
```
※この事象はcds2typesのGitリポジトリにissueとして上がっています。
https://github.com/mrbandler/cds2types/issues/52


#### 6.5. デプロイ
```
cf deploy mta_archives/bookshop-ts_1.0.0.mtar
```

## 動作確認
.httpファイルを作成してテストを行うと、デプロイしたイベントハンドラが正しく動いていることが確認できます。

```
@server = https://...trial-dev-bookshop-ts-srv.cfapps.us10-001.hana.ondemand.com


###
GET {{server}}/browse/ListOfBooks

### 
POST {{server}}/browse/submitOrder
Content-Type: application/json

{ "book":201, "quantity":5 }
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/01b24c19-562f-7dbc-8aab-b36764db9fd5.png)

## 感想
正直なところ私はそれほどメリット（TypeScriptを使わないときと比べてよくなった！）を感じませんでした。[UI5でType Scriptを使った](https://qiita.com/tami/items/fa703b212982a094b7f5)ときは、従来だとできなかったES6に対応した書き方ができるため良さを感じたのですが、CAPの場合はTypeScriptを使わなくてもそれができる、ということが主な理由です。
今回使用したサービスはエンティティや項目数も限られたものでしたが、もっと大きいプロジェクトでは型の定義をちゃんとしておくことで開発時のミスを減らせる（気づける）、というTypeScript本来の利点を感じられるのだろうと思います。

## 参考
- [capire: Using TypeScript](https://cap.cloud.sap/docs/node.js/typescript)
- [Full Stack TypeScript App for Cloud Foundry – Sample Repository](https://blogs.sap.com/2021/12/09/full-stack-typescript-app-for-cloud-foundry-sample-repository/)
- [SAP-samples/btp-full-stack-typescript-app](https://github.com/SAP-samples/btp-full-stack-typescript-app)
- [Taking CAP to the next level – with TypeScript](https://blogs.sap.com/2020/05/22/taking-cap-to-the-next-level-with-typescript/)
- [HeneryHawk/cap-bookshop-typescript](https://github.com/HeneryHawk/cap-bookshop-typescript)