1. 記事一覧 >
  2. ブログ記事
Dataverse
category logo

【MSAL】Dataverse Web APIで追加・更新・削除・画像アップロード・画像ダウンロード

(更新) (公開)

はじめに

前回、「【MSAL】Dataverse Web API から認可コードフロー+ PKCE で自分のデータだけを取得」にて、learn.microsoft の「クイック スタート: msal.js を使用して Dataverse の SPA アプリケーションを登録して構成する」をやってみました。
これにより、Azure AD ユーザーのサインインを行い、PKCE による承認コード フローを使用し、アクセストークンを使って、Dataverse(Dynamics 365 / Dataverse Web API)から直接データを取得することに成功しました。
今回は、その続きで、Microsoft Dataverse の画像列有りの独自テーブルに API で レコードの 追加・更新・削除・画像アップロード・画像ダウンロード を実施していきます。


サンプルアプリは、index.html のみで、HTML & Vanilla JavaScript(素のJavaScript)です。CDN から Microsoft Authentication Library (MSAL) を読み込んでいます。


MSAL Dataverse 追加・更新・削除・画像アップロード・画像ダウンロード 図


本記事情報の誤りにより何らかの問題が生じても、一切責任を負いません。

自己責任でお願いします。

2022年10月現在の状況を元に説明しています。


テーブル作成

まず、管理者で Power Apps ホーム(https://make.powerapps.com/)にて、Dataverse を開いて、独自テーブルを作成します。以下のように単純なテーブルとします。
作成の仕方は、別記事「Dataverse に独自テーブルを作って自分で登録したレコードしか閲覧編集できないようにする」に詳しく書きましたので、端折ります。


テーブル名: 画像テーブル
スキーマ名: gazo

新しいテーブル


Name(プライマリ列)image
文字列画像(画像の最大サイズ=上限の 30720KB)

新しい列


動作内容

動作内容を先に書きます。

画面は、元々の learn.microsoftindex.html を最小限書き換えただけで、かなり地味です。
ログインしたら、以下のボタンが現れます。


ログアウト: ログアウトします。(動作内容の紹介からは、省略します。)
Post(新規レコード追加): Name 列に insert-適当な文字列 を入れて、新規レコードを追加します。
Get(テーブルデータ取得): gazoId 列(自動作成・参照のみ)、Name 列、image 列の値をテーブルで表示します。
Patch(テーブルデータ更新): 指定した gazoId に該当するレコードの Name 列に update-適当な文字列 を入れて、更新します。
ファイルを選択/画像アップロード: 選択した画像ファイルを image 列に登録します。
Get Image(画像ダウンロード&表示): 指定した gazoId に該当するレコードの Image 列の画像を表示します。
Delete(行削除): 指定した gazoId に該当するレコードを削除します。


最初にログイン(Azure AD にサインイン)して、やっていきます。
このとき、当然ですが、サインインしたユーザーに レコードの 閲覧・追加・更新・削除・画像アップロード・画像ダウンロード の権限はあるものとします。

Loginボタン
サインイン
サインイン後


Post(新規レコード追加)

MethodPOST


URLhttps://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos

https://***********.api.crm*.dynamics.com/api/data/v9.2 は、Power Apps右上の歯車 → 開発者リソース → Web API エンドポイント に表示されるURLです。

右上歯車
xxxxx_gazos は、Power Apps - Dataverse → テーブル の名前列に表示されるテーブル名です。以降、xxxxx_gazos と出てきたら、対象テーブル名指定の意味です。

大文字は、小文字に。さらに、複数形で書く必要があります。

xxxxx_ のようにプレフィックスが付いている場合、プレフィックスも必要です。例えば、gazos だけ指定するのは、NGです。

対象テーブル名

Header

ヘッダ名
AuthorizationBearer アクセストークン
Content-Typeapplication/json

Body{"xxxxx_name":"insert-500fT7sn"}


Body は、JSON 文字列です。
{"列名":"値","列名":"値",...} と書きます。
ここで、いきなり、画像列に画像を入れられません。画像列に画像をアップロードするときに、gazoId の値の指定が必要だからです。
モデル駆動型アプリでも "このレコードはまだ作成されていません。画像のアップロードを有効にするには、このレコードを作成してください" と表示されます。
このレコードはまだ作成されていません。画像のアップロードを有効にするには、このレコードを作成してください


結果

HTTP ステータスコードを表示するようにしています。
成功したら、204 No Content が返ってきます。

204 No Content


Dataverse で確認すると、1レコード追加されています。

Dataverseで確認すると、1レコード追加されている

画像テーブル列(gazoId列)とimage列を表示に追加しています。

画像テーブル列(gazoId列)とimage列を表示に追加

その他

こんなことして良いのか謎ですが、実は、
Body: {"xxxxx_gazoid":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "xxxxx_name":"insert-mnxYvPcU"}
のように、直接 gazoId の値を指定できます。(gazoId 列は、参照オンリー、かつ、値は自動生成のはずですが。)
gazoId の値のハイフンの位置がおかしかったり、文字数がおかしかったり、16進数文字列ではない場合、NGのようです。
直接gazoIdを指定した結果


Get(テーブルデータ取得)

MethodGET


URLhttps://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos?$select=xxxxx_gazoid,xxxxx_name,xxxxx_image

xxxxx_gazo テーブルの xxxxx_gazoidxxxxx_namexxxxx_image 列のレコードを全て抽出します。絞り込み出来ますが、ここでは、絞り込みを行っていません。

列名は、Power Apps - Dataverse → テーブル → 画像テーブル → スキーマ - 列 の名前列に表示される列名です。

列名の大文字は、小文字で書く必要があります。

xxxxx_ のようにプレフィックスが付いている場合、プレフィックスも必要です。例えば、gazoid だけ指定するのは、NG(400 Bad Request)です。

Power Apps - Dataverse → テーブル → 画像テーブル → スキーマ - 列

Header

ヘッダ名
AuthorizationBearer アクセストークン
Content-Typeapplication/json
OData-MaxVersion4.0
OData-Version4.0

Body無し


結果

レコードの内容が表示されます。まだ画像登録していませんので、image 列は、空白です。

レコードの内容 image列は、空白


もう一度 Post(新規レコード追加) → Get(テーブルデータ取得) とすると、1レコード追加されています。

もう一度 Post(新規レコード追加) → Get(テーブルデータ取得)


Patch(テーブルデータ更新)

MethodPatch


URLhttps://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)

xxxxx_gazos(d78b4***-****-****-****-*******279c8) は、対象テーブル名(gazoId 列の値) です。

index.html(後述)では、直接ソースコードに書き込みます。


Header

ヘッダ名
AuthorizationBearer アクセストークン
Content-Typeapplication/json

Body{"xxxxx_name":"upate-nzCfxbP3"}


Body は、JSON 文字列です。
{"列名":"値","列名":"値",...} と書きます。


結果

成功したら、204 No Content が返ってきます。

Patch(テーブルデータ更新)成功


Dataverse で確認すると、Name 列が更新されています。

Dataverseで確認すると、Name列が更新されている


Get(テーブルデータ取得)で確認しても同様です。

Get(テーブルデータ取得)で確認 Name列が更新されている


その他

POST と同じく、予想外の挙動をしました。
該当の gazoId が無い場合、NGではなく、レコード追加の動きになります。
この挙動により、POST と同じく、
URL: https://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb)
Body: {"xxxxx_name":"update-z7du9w6p"}
のように、直接 gazoId の値を指定できます。
ここでもやはり、gazoId の値のハイフンの位置がおかしかったり、文字数がおかしかったり、16進数文字列ではない場合、NG(400 Bad Request)です。
該当の gazoId が無い場合、NGではなく、レコード追加の動き

該当の gazoId が無い場合、NGではなく、レコード追加の動きに関しては、OData v4の仕様です。

画像アップロード

MethodPatch


URLhttps://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)/xxxxx_image

xxxxx_image は、画像型の列です。


Header

ヘッダ名
AuthorizationBearer アクセストークン
x-ms-file-name画像ファイル名
Content-Typeapplication/octet-stream

Body画像のバイナリデータ


x-ms-file-name ヘッダは、必須ではなく、Azure Blob Storage に保存されるときのファイル名のようですが、有り無しの違いは確認できませんでした。
Content-Type: application/octet-stream は必須で、
無い場合、Body が何なのか認識しないらしく、400 Bad Request とともに、以下のレスポンスがありました。

{
  "error": {
    "code": "0x0",
    "message": "Error identified in Payload provided by the user for Entity :'', For more information on this error please follow this help link https://go.microsoft.com/fwlink/?linkid=2195293  ---->  InnerException : Microsoft.OData.ODataContentTypeException: A missing or empty content type header was found when trying to read a message. The content type header is required.\r\n   at Microsoft.OData.ODataMessageReader.GetContentTypeHeader(ODataPayloadKind[] payloadKinds)\r\n   at Microsoft.OData.ODataMessageReader.ReadFromInput[T](Func`2 readFunc, ODataPayloadKind[] payloadKinds)\r\n   at System.Web.OData.Formatter.Deserialization.ODataPrimitiveDeserializer.Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)\r\n   at Microsoft.Crm.Extensibility.ODataV4.CrmODataBinaryTypeDeserializer.Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)\r\n   at System.Web.OData.Formatter.ODataMediaTypeFormatter.ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)."
  }
}

image/pngの場合、以下のエラーでした。

{
  "error": {
    "code": "",
    "message": "The request entity's media type 'image/png' is not supported for this resource."
  }
}

また、普通にフォームを使ってアップロードする(multipart/form-data)のはダメで、バイナリデータを Body にセットする必要がありました。


結果

成功したら、204 No Content が返ってきます。

ファイルを選択
開く
画像アップロード
Status: 204


Dataverse で確認すると、サムネイル画像が表示されます。

Dataverseで確認すると、サムネイル画像が表示される


Get(テーブルデータ取得)で確認すると、image 列は、サムネイル画像(144×144)の Base64 文字列が取得されます。

Get(テーブルデータ取得)で確認すると、Base64文字列


その他

一度に送信できるのは、最大 16MB で、それ以外の場合、チャンク(分割)して送信する必要があるような記述が見られましたが、約 30MB の画像をアップロードしても問題ありませんでした。(画像の最大サイズ=上限の 30720KB で列を作成しています。)
https://learn.microsoft.com/ja-jp/power-apps/developer/data-platform/image-attributes
16MB を超えるイメージに対してチャンク アップロードを使用するという制限がなくなりました。 とあります。
16MB を超えるイメージに対してチャンク アップロードを使用するという制限がなくなりました。


30MB 超の場合、さすがに、400 Bad Request が返りました。


Body に null を入れて再アップロードすると、画像が消えました。


Get Image(画像データダウンロード&表示)

MethodGET


URLhttps://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)/xxxxx_image/$value?size=full

$value?size=full 部分は固定です。?size=full が無い場合、サムネイル画像(144×144)が返ります。


Header

ヘッダ名
AuthorizationBearer アクセストークン
Content-Typeapplication/octet-stream

Body無し


結果

response.blob() → バイナリデータ → createObjectURL()imgタグurl=に指定で、画像表示されます。

imgタグで画像表示


Delete(行削除)

MethodDELETE


URLhttps://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)


Header

ヘッダ名
AuthorizationBearer アクセストークン

Body無し


結果

成功したら、204 No Content が返ってきます。


レコードが消えました。

DataverseでDelete(行削除)確認


Get(テーブルデータ取得)で確認しても、やはり消えています。

Get(テーブルデータ取得)で確認しても、消えている


その他

該当レコードが無い場合、404 Not Found で、以下のレスポンスがあります。

{
  "error": {
    "code": "0x80040217",
    "message": "xxxxx_gazo With Id = d78b4***-****-****-****-*******279c8 Does Not Exist"
  }
}

準備

下記、一連の手順は、別記事「Dataverse に独自テーブルを作って自分で登録したレコードしか閲覧編集できないようにする」「【MSAL】Dataverse Web API から認可コードフロー+ PKCE で自分のデータだけを取得」に詳しく書きましたので、細かい手順は、端折ります。


セキュリティロール

テストユーザーに閲覧・追加・更新・削除の権限を与えます。


これは、テストしたいユーザーに「画像テーブル」への閲覧・追加・更新・削除権限を与えるもので、全てのデータを閲覧・追加・更新・削除できるような管理者のみでテストを実施する場合は、必要ありません。


アプリケーションの登録

Azure AD(Azure Active Directory) の アプリの登録 にアプリ(今回作成する Web クライアント index.html 用の設定)を登録します。


名前simplespa-application-msal-js(任意)
サポートされているアカウントの種類この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)
リダイレクト URIシングルページアプリケーション (SPA) http://localhost:5500/index.html
で登録し、


概要 に表示されるものは以下とします。
アプリケーション (クライアント) ID: 94228***-****-****-****-*******f77ec
ディレクトリ (テナント) ID: 0c6c0***-****-****-****-*******d0d2b


API のアクセス許可 にて、 Dynamics CRMuser_impersonationアクセス許可の追加 済みとします。

API のアクセス許可


Live Server

今回作成するのは、index.html です。これを localhost:5500 の Web サーバーで見られるようにします。
今回は、Live Server で実現します。


VSCode(Visual Studio Code)を起動して、拡張機能 Live Server をインストールします。


設定 liveServer.settings.hostlocalhost に変更します。


Web アプリ作成

Web アプリ といっても、index.html 一個だけです。index.htmlのことを「アプリ」と言っています。


適当な場所に simplespa(名称は任意) という空フォルダを作成し、ファイルフォルダーを開く で選択します。 フォルダーを開く


フォルダ選択


右クリック → 新しいファイル...
index.html を作成します。 新しいファイル


index.html作成


この内容を以下のようにして、保存します。

index.html
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script>
      const baseUrl = 'https://org.api.crm.dynamics.com'; //<= Change this
      const clientId = '11111111-1111-1111-1111-111111111111'; //<= Change this
      const tenantId = '22222222-2222-2222-2222-222222222222'; //<= Change this
      const redirectUrl = 'http://localhost:5500/index.html';
      const webAPIEndpoint = baseUrl + '/api/data/v9.2';

      const id = '33333333-3333-3333-3333-333333333333';
      var dataverseUrls = new Array();
      dataverseUrls.Post = 'xxxxx_gazos';
      dataverseUrls.Get =
        'xxxxx_gazos?$select=xxxxx_gazoid,xxxxx_name,xxxxx_image';
      dataverseUrls.Patch = `xxxxx_gazos(${id})`;
      dataverseUrls.UploadImage = `xxxxx_gazos(${id})/xxxxx_image`;
      dataverseUrls.GetImage = `xxxxx_gazos(${id})/xxxxx_image/$value?size=full`;
      dataverseUrls.Delete = `xxxxx_gazos(${id})`;

      const msalConfig = {
        auth: {
          clientId: clientId,
          authority: 'https://login.microsoftonline.com/' + tenantId,
          redirectUri: redirectUrl,
        },
        cache: {
          cacheLocation: 'sessionStorage',
        },
        system: {
          loggerOptions: {
            loggerCallback: (level, message, containsPii) => {
              if (containsPii) {
                return;
              }
              switch (level) {
                case msal.LogLevel.Error:
                  console.error(message);
                  return;
                case msal.LogLevel.Info:
                  console.info(message);
                  return;
                case msal.LogLevel.Verbose:
                  console.debug(message);
                  return;
                case msal.LogLevel.Warning:
                  console.warn(message);
                  return;
              }
            },
          },
        },
      };
    </script>
    <script
      type="text/javascript"
      src="https://alcdn.msauth.net/browser/2.28.1/js/msal-browser.min.js"
    ></script>
    <style>
      body {
        font-family: 'Segoe UI';
      }
      table {
        border-collapse: collapse;
      }
      td,
      th {
        border: 1px solid black;
      }
      #message {
        color: green;
      }
    </style>
  </head>
  <body>
    <div>
      <button id="loginButton" onclick="signIn()">Login</button>
    </div>
    <div id="buttons" style="display: none">
      <button onclick="signOut()">ログアウト</button>
      <button onclick="executeDataverseAPI('Post', writeResultStatus)">
        Post(新規レコード追加)
      </button>
      <button onclick="executeDataverseAPI('Get', writeTable)">
        Get(テーブルデータ取得)
      </button>
      <button onclick="executeDataverseAPI('Patch', writeResultStatus)">
        Patch(テーブルデータ更新)
      </button>
      <form method="post" action="">
        <input type="file" name="input_file" />
        <input
          type="submit"
          id="btn_submit"
          name="btn_submit"
          value="画像アップロード"
        />
      </form>
      <button onclick="executeDataverseAPI('GetImage', renderImage)">
        Get Image(画像ダウンロード&表示)
      </button>
      <button onclick="executeDataverseAPI('Delete', writeResultStatus)">
        Delete(行削除)
      </button>
      <div id="message"></div>
      <div id="status"></div>
      <table id="gazosTable" style="display: none">
        <thead>
          <tr>
            <th>gazoId</th>
            <th>name</th>
            <th>image</th>
          </tr>
        </thead>
        <tbody id="gazosTableBody"></tbody>
      </table>
      <img id="imageBody" />
    </div>
    <script>
      const loginButton = document.getElementById('loginButton');
      const buttons = document.getElementById('buttons');
      const gazosTable = document.getElementById('gazosTable');
      const gazosTableBody = document.getElementById('gazosTableBody');
      const imageBody = document.getElementById('imageBody');
      const message = document.getElementById('message');
      const status = document.getElementById('status');
      window.addEventListener('DOMContentLoaded', () => {
        const btn_submit = document.getElementById('btn_submit');
        btn_submit.addEventListener('click', (e) => {
          e.preventDefault();
          executeDataverseAPI('UploadImage', writeResultStatus);
        });
      });
      const myMSALObj = new msal.PublicClientApplication(msalConfig);
      let username = '';

      function selectAccount() {
        const currentAccounts = myMSALObj.getAllAccounts();
        if (currentAccounts.length === 0) {
          return;
        } else if (currentAccounts.length > 1) {
          console.warn('Multiple accounts detected.');
        } else if (currentAccounts.length === 1) {
          username = currentAccounts[0].username;
          showWelcomeMessage(username);
        }
      }

      function signIn() {
        myMSALObj
          .loginPopup({
            scopes: ['User.Read', baseUrl + '/user_impersonation'],
          })
          .then((response) => {
            if (response !== null) {
              username = response.account.username;
              showWelcomeMessage(username);
            } else {
              selectAccount();
            }
          })
          .catch((error) => {
            console.error(error);
          });
      }

      function showWelcomeMessage(username) {
        message.innerHTML = `Welcome ${username}`;
        loginButton.style.display = 'none';
        buttons.style.display = 'block';
      }

      function signOut() {
        const logoutRequest = {
          account: myMSALObj.getAccountByUsername(username),
          postLogoutRedirectUri: msalConfig.auth.redirectUri,
          mainWindowRedirectUri: msalConfig.auth.redirectUri,
        };

        myMSALObj.logoutPopup(logoutRequest);
      }

      function getTokenPopup(request) {
        request.account = myMSALObj.getAccountByUsername(username);

        return myMSALObj.acquireTokenSilent(request).catch((error) => {
          console.warn(
            'Silent token acquisition fails. Acquiring token using popup'
          );
          if (error instanceof msal.InteractionRequiredAuthError) {
            return myMSALObj
              .acquireTokenPopup(request)
              .then((tokenResponse) => {
                console.log(tokenResponse);
                return tokenResponse;
              })
              .catch((error) => {
                console.error(error);
              });
          } else {
            console.warn(error);
          }
        });
      }

      function executeDataverseAPI(method, callback) {
        getTokenPopup({
          scopes: [baseUrl + '/.default'],
        })
          .then((response) => {
            dataverseFunctions[method](
              dataverseUrls[method],
              response.accessToken,
              callback
            );
          })
          .catch((error) => {
            console.error(error);
          });
      }

      var dataverseFunctions = new Array();
      // 新規レコード追加(INSERT)
      dataverseFunctions.Post = function (url, token, callback) {
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
        headers.append('Authorization', bearer);
        headers.append('Content-Type', 'application/json');
        const options = {
          method: 'POST',
          headers: headers,
          body: '{"xxxxx_name":"insert-' + makeid(8) + '"}',
        };
        console.log(
          'POST Request made to Dataverse at: ' + new Date().toString()
        );
        fetch(webAPIEndpoint + '/' + url, options)
          .then((response) => callback(response))
          .catch((error) => console.log(error));
      };
      // SELECT
      dataverseFunctions.Get = function (url, token, callback) {
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
        headers.append('Authorization', bearer);
        headers.append('Accept', 'application/json');
        headers.append('OData-MaxVersion', '4.0');
        headers.append('OData-Version', '4.0');
        const options = {
          method: 'GET',
          headers: headers,
        };
        console.log(
          'GET Request made to Dataverse at: ' + new Date().toString()
        );
        fetch(webAPIEndpoint + '/' + url, options)
          .then((response) => response.json())
          .then((response) => callback(response))
          .catch((error) => console.log(error));
      };
      // 更新(UPDATE)
      dataverseFunctions.Patch = function (url, token, callback) {
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
        headers.append('Authorization', bearer);
        headers.append('Content-Type', 'application/json');
        const options = {
          method: 'PATCH',
          headers: headers,
          body: '{"xxxxx_name":"update-' + makeid(8) + '"}',
        };
        console.log(
          'Patch Request made to Dataverse at: ' + new Date().toString()
        );
        fetch(webAPIEndpoint + '/' + url, options)
          .then((response) => callback(response))
          .catch((error) => console.log(error));
      };
      // 画像アップロード
      dataverseFunctions.UploadImage = function (url, token, callback) {
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
        headers.append('Authorization', bearer);
        const input_file = document.querySelector('input[name=input_file]');
        const file = input_file.files[0];
        if (file.type && !file.type.startsWith('image/')) {
          console.log('File is not an image.', file.type, file);
          return;
        }
        const reader = new FileReader();
        // ファイル読み込み開始
        reader.readAsArrayBuffer(file);
        // onload = ファイルの読み込み完了
        reader.onload = (event) => {
          const bin = event.currentTarget.result; // 読み込んだ結果=バイナリデータ
          headers.append('x-ms-file-name', file.name);
          headers.append('Content-Type', 'application/octet-stream');
          console.log(
            'UPLOAD Request made to Dataverse at: ' + new Date().toString()
          );
          fetch(webAPIEndpoint + '/' + url, {
            method: 'PATCH',
            headers: headers,
            body: bin,
          })
            .then((response) => callback(response))
            .catch((error) => console.log(error));
        };
      };
      // 画像ダウンロード→表示
      dataverseFunctions.GetImage = function (url, token, callback) {
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
        headers.append('Authorization', bearer);
        headers.append('Content-Type', 'application/octet-stream');
        const options = {
          method: 'GET',
          headers: headers,
        };
        console.log('GET Image from Dataverse at: ' + new Date().toString());
        fetch(webAPIEndpoint + '/' + url, options)
          .then((response) => response.blob())
          .then((response) => callback(response))
          .catch((error) => console.log(error));
      };
      // 指定行削除(DELETE)
      dataverseFunctions.Delete = function (url, token, callback) {
        const headers = new Headers();
        const bearer = `Bearer ${token}`;
        headers.append('Authorization', bearer);
        const options = {
          method: 'DELETE',
          headers: headers,
        };
        console.log(
          'DELETE Request made to Dataverse at: ' + new Date().toString()
        );
        fetch(webAPIEndpoint + '/' + url, options)
          .then((response) => callback(response))
          .catch((error) => console.log(error));
      };
      // 適当な文字列生成
      function makeid(length) {
        var result = '';
        var characters =
          'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        var charactersLength = characters.length;
        for (var i = 0; i < length; i++) {
          result += characters.charAt(
            Math.floor(Math.random() * charactersLength)
          );
        }
        return result;
      }
      // HTTPステータス表示
      function writeResultStatus(result) {
        console.log('result: ', result);
        status.innerHTML = 'Status: ' + result.status;
        gazosTable.style.display = 'none';
        status.style.display = 'block';
        imageBody.style.display = 'none';
      }
      // テーブル描画
      function writeTable(data) {
        while (gazosTableBody.lastChild) {
          gazosTableBody.removeChild(gazosTableBody.lastChild);
        }
        data.value.forEach(function (record) {
          var gazoidCell = document.createElement('td');
          gazoidCell.textContent = record.xxxxx_gazoid;
          var nameCell = document.createElement('td');
          nameCell.textContent = record.xxxxx_name;
          var imageCell = document.createElement('td');
          imageCell.textContent = record.xxxxx_image;
          var row = document.createElement('tr');
          row.appendChild(gazoidCell);
          row.appendChild(nameCell);
          row.appendChild(imageCell);
          gazosTableBody.appendChild(row);
        });
        gazosTable.style.display = 'block';
        status.style.display = 'none';
        imageBody.style.display = 'none';
      }
      // 画像描画
      function renderImage(data) {
        const url = (window.URL || window.webkitURL).createObjectURL(data);
        gazosTable.style.display = 'none';
        status.style.display = 'none';
        imageBody.style.display = 'block';
        imageBody.src = url;
      }
      // Welcome ... 描画
      selectAccount();
    </script>
  </body>
</html>

index.htmlへペースト


const baseUrl = "https://org.api.crm.dynamics.com"; の部分を書き換えます。


const baseUrl は、API の URL です。
これは、Power Apps 右上歯車 → 開発者リソース で分かります。

右上歯車


開発者リソース

Web API エンドポイント のところに
https://***********.api.crm*.dynamics.com/api/data/v9.2
が表示されている場合、
const baseUrl = "https://***********.api.crm*.dynamics.com";
とします。


const clientId = "11111111-1111-1111-1111-111111111111";
const tenantId = "22222222-2222-2222-2222-222222222222";
部分は、
アプリケーションの登録の時に控えた
アプリケーション (クライアント) ID: 94228***-****-****-****-*******f77ec
ディレクトリ (テナント) ID: 0c6c0***-****-****-****-*******d0d2b
です。
const clientId = "94228***-****-****-****-*******f77ec";
const tenantId = "0c6c0***-****-****-****-*******d0d2b";
とします。


アプリのデバッグ

VS Code 右下に、Go Live ボタンがあります。

Go Liveボタン

これを押すだけで、http://localhost:5500 の Web サーバーが立ち上がって、http://localhost:5500/index.html の動作確認ができます。


テーブル名(xxxxx_gazos
列名(xxxxx_name,xxxxx_image
対象レコードID const id = '33333333-3333-3333-3333-333333333333';
は、適切に書き換えが必要です。書き換えたら、リロードされて反映されます。


Good Luck!


loading...