SAP Cloud SDK for JavaScriptを使ってみる （2）サービスの作成

##はじめに
この記事は、[SAP Cloud SDK for JavaScriptを使ってみる](https://qiita.com/tami/items/a0280ea080b24f087e68)シリーズの2回目です。この記事では、SAP Cloud SDK を使ってサービスを開発する手順について説明します。

ベースとするのは以下のチュートリアルです。

- [Create an App Using SAP Cloud SDK for JavaScript](https://developers.sap.com/group.s4sdk-js-cloud-foundry.html)
- [Build an Address Manager with the SAP Cloud SDK's OData Virtual Data Model](https://developers.sap.com/group.cloudsdk-js-vdm.html)

チュートリアルでは、S/4HANAのOData APIである[Business Partner](https://api.sap.com/api/API_BUSINESS_PARTNER/resource)を使い、SAP Cloud SDKのOData Clientの使い方を確認します。OData Clientの使い方自体はチュートリアルで詳しく説明されていますので、そちらを参考にしてください。
この記事では、NestJSの[CLI](https://docs.nestjs.com/cli/usages)を使って必要なファイルを作る方法について説明します。

##事前準備
テストで使えるS/4HANAの環境がない場合、接続先としては以下のオプションがあります。
①API Business Hub
②モックのBusiness Partner Serviceをローカルに作成
今回は、テストを行う都合上、結果が一定になる②の方法を選択します。

モックのBusiness Partner Serviceの作り方については、以下に書いてあります。
https://sap.github.io/cloud-s4-sdk-book/pages/mock-odata.html

簡単に説明すると、

- [GitHub](https://github.com/SAP/cloud-s4-sdk-book)のブランチ`mock-server`をチェックアウト

```
git clone https://github.com/SAP/cloud-s4-sdk-book
cd cloud-s4-sdk-book
git checkout mock-server
```

- モックサーバーを起動

```
npm i
npm start
```

`http://localhost:3000/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner`にアクセスすると以下のようにデータが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0f73eb49-ce51-148f-ea82-2848342d8674.png)


##ステップ
1. プロジェクトを生成
1. Module、Controller、Serviceを生成
1. Business Partner取得のロジックを実装
1. Cloud Foundryにデプロイ

###1. プロジェクトを生成
前提：`npm install -g @sap-cloud-sdk/cli`でSAP Cloud SDK CLIがインストールされていること

####1.1. プロジェクトを生成

```
sap-cloud-sdk init my-sdk-project
```
一つ目の質問（nest.jsのプロジェクトをターゲットのディレクトリで初期化するか）には`y`を選択します。
二つ目の質問（usage analyticsを提供するか）には`y`または`n`を選択します。

```
The target directory (my-sdk-project) does not contain a `package.json.`
Should a new `nest.js` project be initialized in the target directory? (y|n): y
Building application scaffold... done
Enter project name (for use in manifest.yml) [my-sdk-project]: 
Do you want to provide anonymous usage analytics to help us improve the SDK? (y|n): y
```
最終的に以下の画面が表示されます。

```
+--------------------------------------------------------------+
 ✅ Init finished successfully.

 🚀 Next steps:
 - Run the application locally (`npm run start:dev`)
 - Deploy your application (`npm run deploy`)

 🔨 Consider setting up Jenkins to continuously build your app.
 Use `sap-cloud-sdk add-cx-server` to create the setup script.
+--------------------------------------------------------------+
```

####1.2. src/main.jsを変更
デフォルトでは、生成されたアプリケーションはポート3000で動くようになっていますが、モックサーバーをポート3000で実行している都合で、ポートを8080に変更します。

```javascript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT || 3000); //->8080に変更
}
bootstrap();
```

####1.3. 実行してみる
ターミナルに`npm run start:dev`と入力し、ブラウザで`http://localhost:8080/`を開きます。Hello Worldの画面が表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0efdf90b-1642-fc66-60a2-a190ca0656e7.png)

###2. Module、Controller、Serviceを生成
続いて、Module、Controller、Serviceを生成します。Module、Controller、Serviceの関係については[前回](https://qiita.com/tami/items/a0280ea080b24f087e68#sap-cloud-sdk-for-java-script%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%A7%8B%E6%88%90)の記事をご参照ください。

####2.1. Moduleを生成
以下のコマンドでModuleを生成します。

```
nest generate module business-partner
```
srcフォルダの配下にbusiness-partnerというフォルダとモジュールができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/aebc31d5-1a03-5698-e5aa-7e83eeab8d68.png)
app.module.tsのimportsにBusinessPartnerModuleが追加されます。

```javascript:src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BusinessPartnerModule } from './business-partner/business-partner.module';

@Module({
  imports: [BusinessPartnerModule], //自動で追加される
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

####2.2. Controllerを生成
以下のコマンドでControllerを生成します。

```
nest generate controller business-partner
```

business-partnerフォルダ配下にcontrollerとつくファイルが2つ追加されました。このうち、`spec.ts`はユニットテストで使うためのファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c4472f3a-ca3d-b49f-80c6-03b403f89ccc.png)
business-partner.module.tsにControllerが自動で追加されました。

```javascript:src/business-partner/business-partner.module.ts
import { Module } from '@nestjs/common';
import { BusinessPartnerController } from './business-partner.controller';

@Module({
  controllers: [BusinessPartnerController]
})
export class BusinessPartnerModule {}
```

####2.3. Serviceを生成
以下のコマンドでServiceを生成します。

```
nest generate service business-partner
```
serviceとつくファイルが2つ追加されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/29eedca8-d6fe-c3e7-a5ff-4cadad234347.png)

business-partner.module.tsにServiceが自動で追加されました。

```javascript:src/business-partner/business-partner.module.ts
import { Module } from '@nestjs/common';
import { BusinessPartnerController } from './business-partner.controller';
import { BusinessPartnerService } from './business-partner.service';

@Module({
  controllers: [BusinessPartnerController],
  providers: [BusinessPartnerService]
})
export class BusinessPartnerModule {}
```

###3. Business Partner取得のロジックを実装
SAP Cloud SDKのOData Clientを使い、Business Partnerを取得します。

####3.1. [@sap/cloud-sdk-vdm-business-partner-service](https://help.sap.com/doc/98598d91753e472da1235b2aa36babc5/1.0/en-US/modules/_sap_cloud_sdk_vdm_business_partner_service.html)をインストール
以下のコマンドでBusiness PartnerのODataサービスにアクセスするためのライブラリをインストールします。

```
npm i @sap/cloud-sdk-vdm-business-partner-service
```

####3.2. Serviceの実装
Serviceに以下のロジックを実装します。

```javascript:src/business-partner/business-partner.service.ts
import { Injectable } from '@nestjs/common';
import { BusinessPartner } from '@sap/cloud-sdk-vdm-business-partner-service'

@Injectable()
export class BusinessPartnerService {
    getAllBusinessPartners(): Promise<BusinessPartner[]> {
        return BusinessPartner.requestBuilder()
            .getAll()
            .execute({
                destinationName: 'MockServer'
            })
    }
}
```

ここでは以下のことを行っています。

- `@sap/cloud-sdk-vdm-business-partner-service`から`BusinessPartner`をインポート（2行目）
- `getAllBusinessPartners`メソッドでBusiness Partnerを取得
    - [GetAll](https://sap.github.io/cloud-sdk/docs/js/features/odata/use-odata-v2-type-safe-client-for-javascript-typescript#getall-request-builder) Request Builderで全てのエンティティを取得するリクエストを作成
    - executeでHTTPリクエストを実行

**executeについて**
[execute](https://sap.github.io/cloud-sdk/api/1.45.0/modules/sap_cloud_sdk_core#execute)メソッドには[Destination](https://sap.github.io/cloud-sdk/api/1.45.0/interfaces/sap_cloud_sdk_core.destination)オブジェクト、または[DestinationNameAndJwt](https://sap.github.io/cloud-sdk/api/1.45.0/interfaces/sap_cloud_sdk_core.destinationnameandjwt)オブジェクトを渡します。上の例では、BTPのDestinationを使用するためDestinationNameAndJwtを使用しています。URLを直接指定する場合はDestinationオブジェクトを使い以下のようにURLを設定します。

```javascript
            .execute({
                url: 'http://localhost:3000'
            })
```
**ローカル実行のための設定**
ローカル実行時にDestinationが取得できるように、以下の設定を行います。

①.envファイルを作成
プロジェクトのルートに.envファイルを作成し、Destinationの設定をします。

```text:.env
destinations=[{"name": "MockServer", "url": "http://localhost:3000"}]
```

②[@nestjs/config](https://www.npmjs.com/package/@nestjs/config)をインストール

```
npm i @nestjs/config
```

③ConfigModuleをapp.mocule.tsに追加
.envファイルが環境変数として使用されるように、@nestjs/configのConfigModuleをapp.mocule.tsに追加します。

```javascript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; //追加
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BusinessPartnerModule } from './business-partner/business-partner.module';

@Module({
  imports: [ConfigModule.forRoot(), BusinessPartnerModule], //追加
```

####3.3. Controllerの実装
Controllerに以下のロジックを実装します。

```javascript:src/business-partner/business-partner.controller.ts
import { Controller, Get } from '@nestjs/common';
import { BusinessPartnerService } from './business-partner.service';
import { BusinessPartner } from '@sap/cloud-sdk-vdm-business-partner-service'

@Controller('business-partner')
export class BusinessPartnerController {
    constructor(private readonly businessPartnerService: BusinessPartnerService) {}

    @Get()
    getAllBusinessPartners(): Promise<BusinessPartner[]> {
        return this.businessPartnerService.getAllBusinessPartners();
    }
}
```
ここでは以下のことを行っています。

- `@nestjs/common`から`Controller`および`Get`をインポート
- `@sap/cloud-sdk-vdm-business-partner-service`から`BusinessPartner`をインポート
- BusinessPartnerControllerクラスのコンストラクタでBusinessPartnerServiceを使用することを宣言（※）
- `getAllBusinessPartners`メソッドでBusinessPartnerServiceのgetAllBusinessPartnersメソッドを呼ぶ


※これにより、実行時にNestJSが自動的にBusinessPartnerServiceをインスタンス化して挿入してくれる。このことを[Dependency Injection](https://docs.nestjs.com/providers#dependency-injection)という。この仕組みのおかげで、Unit Testで依存先のモジュールをテスト用のモジュールに簡単に置き換えることができる。

**Controller、Getについて**
これらはいずれもstring型のパスを引数に取ることができます。
Controllerで指定したパスがリクエストのベースパスに一致する場合、そのControllerで処理が行われます。同様に、Getで指定したパスがベースパスの次のパスに一致する場合、そのGetの下のメソッドで処理が行われます。
以下の例では、`/business-partners/getAll`というリクエストがgetAllBusinessPartnersメソッドで処理されます。

```javascript
@Controller('business-partner')
export class BusinessPartnerController {
    constructor(private readonly businessPartnerService: BusinessPartnerService) {}

    @Get('getAll')
    getAllBusinessPartners(): Promise<BusinessPartner[]> {
        return this.businessPartnerService.getAllBusinessPartners();
    }
}
```

####3.4. 実行してみる
ブラウザに`http://localhost:8080/business-partner`と入力します。
以下の画面が表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b3622004-836e-91e0-676b-1d899aa1c685.png)

####3.4. 追加のクエリオプションの指定
[OData Client](https://sap.github.io/cloud-sdk/docs/js/features/odata/use-odata-v2-type-safe-client-for-javascript-typescript)では、select, filter, top skipなどのクエリオプションを指定することも可能です。

以下のチュートリアル、およびドキュメントを参照して、クエリオプションを試してみてください。

- [Build OData Queries with the SAP Cloud SDK's Virtual Data Model (Step8~)](https://developers.sap.com/tutorials/cloudsdk-js-vdm-getall.html#64382997-d169-4fcd-a278-ef2d803fb956)
- [Use the OData v2 Client](https://sap.github.io/cloud-sdk/docs/js/features/odata/use-odata-v2-type-safe-client-for-javascript-typescript)

###4. Cloud Foundryにデプロイ
####4.1. Destinationを使用するための設定
事前準備で作成したモックのBusiness Partner ServiceをCloud Foundryにデプロイし、Destinationとして登録します。デプロイ、およびDestinationの登録手順については以下のチュートリアルのStep2～4をご参照ください。

[Deploy Application to Cloud Foundry with SAP Cloud SDK for JavaScript](https://developers.sap.com/tutorials/s4sdkjs-deploy-application-cloud-foundry.html#4480d324-eccf-4987-860d-11065aaf7278)

####4.2. Cloud Foundryにデプロイ
Cloud Foundryにデプロイするためには、package.jsonに定義されたdeployスクリプトを実行します。

```
npm run deploy
```

deployスクリプトは以下のように3つのコマンドから成っています。

```
    "deploy": "npm run build && sap-cloud-sdk package && cf push",
```

各コマンドは次のことを行います。

**npm run build**
distフォルダにビルドした結果のファイルを格納する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/200bfdce-8d8c-23fe-82aa-8b9733645b16.png)

**sap-cloud-sdk package**
deploymentフォルダにdistフォルダ、およびpackage.json、package-lock.jsonをコピーする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/711fbfb8-c8c6-5e65-d73a-a3611034441f.png)

**cf push**
manifest.ymlの内容に従い、アプリケーションをCloud Foundryにデプロイする。
manifest.ymlでは、deployment配下のものをデプロイする設定になっている。

```yaml:manifest.yml
applications:
  - name: my-sdk-project
    path: deployment/
    buildpacks:
      - nodejs_buildpack
    memory: 256M
    command: npm run start:prod
    random-route: true
    services:
      - my-destination    
      - my-xsuaa
```