素のJavaScriptからODataを取得してみた

この記事は [chillSAP 夏の自由研究2022、8/21]("https://note.com/chillsap/n/n62740563aee0")の記事として執筆しています。

# やりたかったこと
JavaScriptでS/4HANAのODataでデータを取ってきてHTMLの画面に表示する、ということをやりたかったです。
SAPのUIって、今SAP UI5ないしFiori Elementsに集約されつつあるじゃないですか。
とはいえ、SAPを使う企業さんには会社ごとにお好みのフロントエンドフレームワークがあると思いますし、スクラッチで開発した画面からAPIでSAPのデータを更新したいって需要、あると思うんです。
そういうことができるのかどうか確認するという目的で、（フレームワークとか使っていない）素のJavaScriptでODataのデータを取得できるのか検証してみました。

↓こんな感じでHTMLの画面を作って、
```html
<!DOCTYPE html>
<html>
<head>
    <title>
        Bank Master List
    </title>
</head>

<body>
    <table>
        <tr>
            <td>
                <table class="list" id="BankList">
                    <thead>
                        <tr>
                            <th>BankCountry</th>
                            <th>BankInternalID</th>
                            <th>BankName</th>
                            <th>Region</th>
                            <th>ShortStreetName</th>
                            <th>ShortCityName</th>
                        </tr>
                    </thead>
                    <tbody>

                    </tbody>
                </table>
            </td>
        </tr>
    </table>
    <script src="script.js"></script>
</body>
</html>
```

`script.js`の処理でODataを取ってきて、`<tbody>`の中に埋め込めばいいじゃんって思いました。
が、そこまではたどり着けませんでした。。

あとから思い返せばSAP Cloud SDK for JavaScriptとか使えばよかったんでしょうけど、環境構築とかめんどくさそうだし、所詮APIなんてHTTPリクエストさえ飛ばせればどうとでもなるんでしょ？
HTTPクライアントからリクエスト飛ばしたときはすんなりレスポンス返ってきたから、同じことをJavaScriptでやればいいだけでしょ？
みたいな甘い考えでいましたが、色々試してみて様々な困難に直面しました。
以下あまりまとまりはありませんが試したことの順で記述します。

# XMLHttpRequestを試してみる その１

APIHUBのサンプルコードが載っていたのでその通りに実装してみます。
ただ、BASIC認証を突破するためにAuithorizationヘッダを付与します。
(※検証していた当時は、[SAP API HUB](https://api.sap.com/api/OP_API_BANK_0002/overview)の各ODataサービスのOverviewからTry Outを選択すればサンプルコードを閲覧できたのですが、
当ブログ執筆時点だと見られなくTry outの項目がなくなっています。原因不明。)


```javascript:script.js
window.onload = function(){
// # XMLHttpRequestを試してみる その１
  var data = null;
  var xhr = new XMLHttpRequest();
  xhr.withCredentials = false;
  
  xhr.addEventListener("readystatechange", function () {
    if (this.readyState === this.DONE) {
      console.log(this.responseText);
    }
  });
  
  //setting request method
  //API endpoint for API sandbox 
  xhr.open("GET", "https:[RootURL]/sap/opu/odata4/iwbep/all/srvd_a2x/sap/api_bank_2/0001/Bank?$top=10",);
  //adding request headers
  xhr.setRequestHeader("DataServiceVersion", "2.0");
  xhr.setRequestHeader("Accept", "application/json");
  //↓ののIDPW、実装では暗号化するでしょうけど今は動確なので素の状態
  xhr.setRequestHeader("Authorization", "Basic [SAPのID]:[PW]");
  //sending request
  xhr.send(data);
```

## 結果
ネットワークタブを見る感じエラーが出ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/b064c981-9ae3-b874-ae19-e16e0687707b.png)

Consoleを見てみると、こんなエラーが出ています。
```
Access to XMLHttpRequest at 'https:[RootURL]/sap/opu/odata4/iwbep/all/srvd_a2x/sap/api_bank_2/0001/Bank?$top=10' from origin 'http://localhost:5500' has been blocked by CORS policy
: Response to preflight request doesn't pass access control check:
 No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

CORS制約の制約にひっかかったっぽいです。
CORSが何かについては、↓を御覧ください。

>同一生成元ポリシーというのは、すごく簡単に言うと 「JavaScript で自由にやりとりできるところは、その JavaScript をとってきたところと同一の場所だけに制限する」 ということです。
>同一生成元かどうか判断する時には、ホスト名、スキーム、ポート番号がチェックされます。これらが同じ場合、同一生成元へのアクセスとみなされます。
>この制限があるために、あるサーバーから JavaScript をダウンロードし、そのスクリプトから全く別のサーバーにアクセスしてそこから情報を取得する、ということができなくなります。
https://javascript.keicode.com/newjs/what-is-cors.php

要するに、Webページのサーバ(今回の場合は`http://localhost:5500`)とは異なるソース(`https:[RootURL]/sap/opu/odata4/iwbep/all/srvd_a2x/sap/api_bank_2`)から取得したリソースはJavaScriptで操作できない、というエラーです。
また、エラーの後半にはプリフライトリクエストがどうのこうのと書いてあります。
これがどういうことなのか、↓

>JavaScript で XMLHttpRquest や Fetch API を使って、サーバーに非同期的な HTTP 要求 (リクエスト) を送信する状況を考えます。
もしこのとき、HTTP のメソッドとして GET、POST、HEAD を使い、かつ Accept、 Accept-Language、Content-Type などのいくつか基本的な HTTP ヘッダーがセットされただけのリクエストであれば、この HTTP リクエストを 「単純な HTTP リクエスト」(simple Cross-Origin request) とします。
それ以外のリクエストは全て「単純ではない要求」として区別します。
単純な要求の場合、後述のプリフライト (preflight) が不要です。
(中略)
上記以外の「単純な要求ではない場合」、ブラウザ上の JavaScript ランタイムは、サーバーに事前に自動的に問い合わせ、 アクセスが許可されるかどうか確認します。この事前問合せのことをプリフライト (Preflight) といいます。
プリフライトは、 HTTP の OPTIONS リクエストを使って行われます。
Preflight の要求時には、これから使う HTTP メソッドや HTTP ヘッダーの種類を申告します。
このとき Origin ヘッダーの他、 Access-Control-Request-Method ヘッダや Access-Control-Request-Headers ヘッダを使います。
サーバーはこれらをチェックして、アクセスを許可する場合、Access-Control-Allow-Origin ヘッダの他、 Access-Control-Request-Method ヘッダやAccess-Control-Request-Headers ヘッダを返します。
>https://javascript.keicode.com/newjs/what-is-cors.php#1-1

ここでもう一度Networkタブを見てみましょう。

よく見てみると、リクエストが二回飛んでいます。
Initiator:Preflightとなっているのが、先程引用したプリフライトリクエストというものらしいです。

プリフライトリクエストの中身をよく見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/cdb89f5d-a97e-ca1a-536e-dd88820eb2c3.png)

↑Request Method:Optionsでリクエストしているみたいです。

そして、二回目の本番のリクエストがStatus:CORS Errorでこけています。

# XMLHttpRequestを試してみる その2

プリフライトリクエストを送るリクエスト（複雑な要求）だとだめで、単純な要求になるようになればいいのでは？と思い、先程のソースを、`Accept、 Accept-Language、Content-Type などのいくつか基本的な HTTP ヘッダーがセットされただけのリクエスト`になるようにしてみます。
authorizationヘッダを使わないように、BASIC認証のIDPWをリクエストにセットしてみます。
```javascript:script.js
window.onload = function(){
var data = null;
var xhr = new XMLHttpRequest();
xhr.withCredentials = false;

xhr.addEventListener("readystatechange", function () {
  if (this.readyState === this.DONE) {
    console.log(this.responseText);
  }
});

//setting request method
//API endpoint for API sandbox 
xhr.open("GET", "https:[RootURL]/sap/opu/odata4/iwbep/all/srvd_a2x/sap/api_bank_2/0001/Bank?$top=10",
                true, "ID", "PW");

//sending request
xhr.send(data);
```

## 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/f236ab7f-779e-f55b-5ecb-2ccbb661ba2b.png)

プリフライトリクエストは飛ばなくなったけど、CORS制約で引っかかっているのは相変わらずです。

どうやら、バックエンド側（SAP Netweaver側）で↓こういう設定をしなければならないみたいです。
https://blogs.sap.com/2019/03/24/use-cors-for-your-netweaver-backend-connection-to-cloud-analytics/

ただ他機能の影響を鑑みてこの内容を実機で実行するのは断念しました。
そこで、S/4HANAのAPIにこだわることはやめて、ODataのデモサービスならどんなクライアントからでもアクセスできるのではと思いついて試してみました。

# XMLHttpRequestを試してみる その3
こちらのODataデモサービスを使うことにします。↓
まずはHTTPクライアントで動作確認します。
```http:request
GET https://services.odata.org/Northwind/Northwind.svc/Orders?$top=5
```

```http:response
HTTP/1.1 200 OK
Content-Length: 1959
Connection: close
Content-Type: application/atom+xml;type=feed;charset=utf-8
Date: Wed, 31 Aug 2022 12:37:49 GMT
Server: Microsoft-IIS/10.0
Access-Control-Allow-Headers: Accept, Origin, Content-Type, MaxDataServiceVersion
Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: DataServiceVersion
Cache-Control: private
Content-Encoding: gzip
Expires: Wed, 31 Aug 2022 12:38:49 GMT
Vary: *
X-Content-Type-Options: nosniff
DataServiceVersion: 1.0;
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET

<?xml version="1.0" encoding="utf-8"?>
<feed xml:base="https://services.odata.org/Northwind/Northwind.svc/" 
  xmlns="http://www.w3.org/2005/Atom" 
  xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" 
  xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <id>https://services.odata.org/Northwind/Northwind.svc/Orders</id>
  <title type="text">Orders</title>
  <updated>2022-08-31T12:37:49Z</updated>
  <link rel="self" title="Orders" href="Orders" />
  <entry>
    <id>https://services.odata.org/Northwind/Northwind.svc/Orders(10248)</id>
    <category term="NorthwindModel.Order" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <link rel="edit" title="Order" href="Orders(10248)" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Customer" type="application/atom+xml;type=entry" title="Customer" href="Orders(10248)/Customer" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Employee" type="application/atom+xml;type=entry" title="Employee" href="Orders(10248)/Employee" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Order_Details" type="application/atom+xml;type=feed" title="Order_Details" href="Orders(10248)/Order_Details" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Shipper" type="application/atom+xml;type=entry" title="Shipper" href="Orders(10248)/Shipper" />
    <title />
    <updated>2022-08-31T12:37:49Z</updated>
    <author>
      <name />
    </author>
    <content type="application/xml">
      <m:properties>
        <d:OrderID m:type="Edm.Int32">10248</d:OrderID>
        <d:CustomerID>VINET</d:CustomerID>
        <d:EmployeeID m:type="Edm.Int32">5</d:EmployeeID>
        <d:OrderDate m:type="Edm.DateTime">1996-07-04T00:00:00</d:OrderDate>
        <d:RequiredDate m:type="Edm.DateTime">1996-08-01T00:00:00</d:RequiredDate>
        <d:ShippedDate m:type="Edm.DateTime">1996-07-16T00:00:00</d:ShippedDate>
        <d:ShipVia m:type="Edm.Int32">3</d:ShipVia>
        <d:Freight m:type="Edm.Decimal">32.3800</d:Freight>
        <d:ShipName>Vins et alcools Chevalier</d:ShipName>
        <d:ShipAddress>59 rue de l'Abbaye</d:ShipAddress>
        <d:ShipCity>Reims</d:ShipCity>
        <d:ShipRegion m:null="true" />
        <d:ShipPostalCode>51100</d:ShipPostalCode>
        <d:ShipCountry>France</d:ShipCountry>
      </m:properties>
    </content>
  </entry>
(以下略)
```

動確が済んだところで、JavaScriptのソースでODataのデモサービスにXMLHttpRequestしてみます。
```JavaScript:script.js
window.onload = function(){
var data = null;
var xhr = new XMLHttpRequest();
xhr.withCredentials = false;

xhr.addEventListener("readystatechange", function () {
  if (this.readyState === this.DONE) {
    console.log(this.responseText);
  }
});

xhr.open("GET", "https://services.odata.org/Northwind/Northwind.svc/Orders");
xhr.setRequestHeader("DataServiceVersion", "2.0");
xhr.setRequestHeader("Accept", "application/json");

xhr.send(data);
}
```

## 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/afe11740-611e-070c-9af7-6ff241f0f72e.png)

だめですね。またCORSのエラーが出ていますし、プリフライトリクエストが飛んでいます。
DataServiceVersionヘッダがついているから、単純ではないリクエストになってしまっているみたい。
そしてよく見ると、１回目に試したときとは違ってプリフライトリクエスト自体がStatusCode501になっています。
試しにHTTPクライアントから、Optionsメソッドを投げてみます。

```http:request
OPTIONS https://services.odata.org/Northwind/Northwind.svc/Orders?$top=5 HTTP/1.1
```

```http:response
HTTP/1.1 501 Not Implemented
Content-Length: 195
Connection: close
Content-Type: application/xml;charset=utf-8
Date: Wed, 31 Aug 2022 12:45:50 GMT
Server: Microsoft-IIS/10.0
Access-Control-Allow-Headers: Accept, Origin, Content-Type, MaxDataServiceVersion
Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: DataServiceVersion
Cache-Control: private
Expires: Wed, 31 Aug 2022 12:46:51 GMT
Vary: *
X-Content-Type-Options: nosniff
DataServiceVersion: 1.0;
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET

<?xml version="1.0" encoding="utf-8"?>
<m:error 
  xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <m:code />
  <m:message xml:lang="en-US">Not Implemented</m:message>
</m:error>

```
Chromeのネットワークタブと同じように、501が返ってきてます。
プリフライトリクエストが飛ぶんだけど、ODataのデモサービスがOPTIONメソッドを許可していないからこうなるみたいです。

# XMLHttpRequestを試してみる その4
なので、単純なリクエストになるよう余計なヘッダを外してリトライします。
```JavaScript:script.js
window.onload = function(){
var data = null;
var xhr = new XMLHttpRequest();
xhr.withCredentials = false;

xhr.addEventListener("readystatechange", function () {
  if (this.readyState === this.DONE) {
    console.log(this.responseText);
  }
});

xhr.open("GET", "https://services.odata.org/Northwind/Northwind.svc/Orders");

xhr.send(data);
}
```

## 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/0f395613-3ea4-ebc8-bef4-fb3315aa3f85.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/f84353ba-90c6-dca1-5360-80b99088accb.png)

成功です！

あとは受け取ったデータをHTMLに反映すれば、ODataで取得したデータを画面に表示できるはずです。
~~ただ、どうやってレスポンスから整形してデータを抜き出すのかまでは、わかりませんでした。
やっぱ何らかのフレームワークを使ったほうがいいのかな。。~~

2022/9/5 追記
ODataをJSON形式で受け取ってHTMLに反映することができました。
以下ソースを示します。

# 完成品

```:ファイル構成
.\
├─index.html
└─script.js

```

```html:index.html
<!DOCTYPE html>
<html>

<head>
    <title>
        OData Grid
    </title>
</head>

<body>
    <table>
        <tr>
            <td>
                <table class="list" id="table1">
                </table>
            </td>
        </tr>
    </table>
    <script src="script.js"></script>
</body>

</html>
```

```javascript:script.js
window.onload = function(){
  var data = null;
  var xhr = new XMLHttpRequest();
  xhr.withCredentials = false;

  xhr.addEventListener("readystatechange", function () {
    if (this.readyState === this.DONE) {
//      console.log(this.responseText);
      JSON_Result = JSON.parse(this.responseText);
      // console.log(JSON_Result);
      let tbl = document.getElementById('table1'); 
      var tblBody = document.createElement("tbody");
      var tblHead = document.createElement("thead");
      var tblHeadRow = document.createElement("tr");

      for (var i in JSON_Result.value) {
        // console.log(JSON_Result.value[i])
        var row = document.createElement("tr");
        var tblHeadCell = document.createElement("th");

        for (var j in JSON_Result.value[i]){
          // console.log(j);
          // console.log(JSON_Result.value[i][j]);
          var cell = document.createElement("td");
          var headcell = document.createElement("th");
          var cellText = document.createTextNode(JSON_Result.value[i][j]);

          if(i == 0) {
            headcell.appendChild(document.createTextNode(j));
            tblHeadRow.appendChild(headcell);
          }

          cell.appendChild(cellText);
          row.appendChild(cell);
        }

      if(i == 0) {
        tbl.appendChild(tblHeadRow);
      }

      tblBody.appendChild(row);
      }
      tbl.appendChild(tblBody);
    }
  });

  xhr.open("GET", "https://services.odata.org/Northwind/Northwind.svc/Orders");
// ↓JSON形式でデータを受け取るようにリクエストします
  xhr.setRequestHeader("Accept", "application/json");
  xhr.send(data);
}
```

## 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/048003fe-9f31-1036-5904-718a44f4bbce.png)


# まとめ
ODataさえ扱えれば、SAPアドオンのフロントエンドは何でもいいんじゃね？と思って素のJavaScriptでODataを扱おうとしましたが、様々な困難に直面しました。。
HTTPクライアントからリクエストを送るだけならすんなりいったんですけどね。。

なので、フロントエンドの沼にハマりたくないならは素直にフレームワークなり、Fiori Elementsを使ったほうが無難だと思いました。

2022/9/5 追記
ODataをHTMLに反映するところまでなんとかいきましたが、素のJavaScriptのソースを書いてて「なんかダサいなー」って思いました。
Gridのデータを表示するだけなのに、なんとなくソースコードが冗長なような気がするので、やっぱりモダンでイケてるフレームワーク(VUEとかそういうの)を使いたいと思いました。

以上です。

# 参考
https://javascript.keicode.com/newjs/what-is-cors.php#1-1

https://blogs.sap.com/2019/03/24/use-cors-for-your-netweaver-backend-connection-to-cloud-analytics/

https://services.odata.org/