- 記事一覧 >
- ブログ記事
【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) を読み込んでいます。
本記事情報の誤りにより何らかの問題が生じても、一切責任を負いません。
自己責任でお願いします。
2022年10月現在の状況を元に説明しています。
テーブル作成
まず、管理者で Power Apps ホーム(https://make.powerapps.com/
)にて、Dataverse を開いて、独自テーブルを作成します。以下のように単純なテーブルとします。
作成の仕方は、別記事「Dataverse に独自テーブルを作って自分で登録したレコードしか閲覧編集できないようにする」に詳しく書きましたので、端折ります。
テーブル名: 画像テーブル
スキーマ名: gazo
列:
Name(プライマリ列) | image |
---|---|
文字列 | 画像(画像の最大サイズ=上限の 30720KB) |
動作内容
動作内容を先に書きます。
画面は、元々の learn.microsoft
の index.html
を最小限書き換えただけで、かなり地味です。
ログインしたら、以下のボタンが現れます。
ログアウト: ログアウトします。(動作内容の紹介からは、省略します。)
Post(新規レコード追加): Name 列に insert-適当な文字列
を入れて、新規レコードを追加します。
Get(テーブルデータ取得): gazoId 列(自動作成・参照のみ)、Name 列、image 列の値をテーブルで表示します。
Patch(テーブルデータ更新): 指定した gazoId に該当するレコードの Name 列に update-適当な文字列
を入れて、更新します。
ファイルを選択/画像アップロード: 選択した画像ファイルを image 列に登録します。
Get Image(画像ダウンロード&表示): 指定した gazoId に該当するレコードの Image 列の画像を表示します。
Delete(行削除): 指定した gazoId に該当するレコードを削除します。
最初にログイン(Azure AD にサインイン)して、やっていきます。
このとき、当然ですが、サインインしたユーザーに レコードの 閲覧・追加・更新・削除・画像アップロード・画像ダウンロード の権限はあるものとします。
Post(新規レコード追加)
Method: POST
URL: https://***********.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:
ヘッダ名 | 値 |
---|---|
Authorization | Bearer アクセストークン |
Content-Type | application/json |
Body: {"xxxxx_name":"insert-500fT7sn"}
Body は、JSON 文字列です。{"列名":"値","列名":"値",...}
と書きます。
ここで、いきなり、画像列に画像を入れられません。画像列に画像をアップロードするときに、gazoId
の値の指定が必要だからです。
モデル駆動型アプリでも "このレコードはまだ作成されていません。画像のアップロードを有効にするには、このレコードを作成してください" と表示されます。
結果:
HTTP ステータスコードを表示するようにしています。
成功したら、204 No Content が返ってきます。
Dataverse で確認すると、1レコード追加されています。
画像テーブル列(gazoId列)とimage列を表示に追加しています。
その他:
こんなことして良いのか謎ですが、実は、
Body: {"xxxxx_gazoid":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "xxxxx_name":"insert-mnxYvPcU"}
のように、直接 gazoId
の値を指定できます。(gazoId 列は、参照オンリー、かつ、値は自動生成のはずですが。)gazoId
の値のハイフンの位置がおかしかったり、文字数がおかしかったり、16進数文字列ではない場合、NGのようです。
Get(テーブルデータ取得)
Method: GET
URL: https://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos?$select=xxxxx_gazoid,xxxxx_name,xxxxx_image
xxxxx_gazo
テーブルのxxxxx_gazoid
、xxxxx_name
、xxxxx_image
列のレコードを全て抽出します。絞り込み出来ますが、ここでは、絞り込みを行っていません。列名は、Power Apps - Dataverse → テーブル → 画像テーブル → スキーマ - 列 の名前列に表示される列名です。
列名の大文字は、小文字で書く必要があります。
xxxxx_
のようにプレフィックスが付いている場合、プレフィックスも必要です。例えば、gazoid
だけ指定するのは、NG(400 Bad Request
)です。
Header:
ヘッダ名 | 値 |
---|---|
Authorization | Bearer アクセストークン |
Content-Type | application/json |
OData-MaxVersion | 4.0 |
OData-Version | 4.0 |
Body: 無し
結果:
レコードの内容が表示されます。まだ画像登録していませんので、image 列は、空白です。
もう一度 Post(新規レコード追加) → Get(テーブルデータ取得) とすると、1レコード追加されています。
Patch(テーブルデータ更新)
Method: Patch
URL: https://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)
xxxxx_gazos(d78b4***-****-****-****-*******279c8)
は、対象テーブル名(gazoId 列の値)
です。
index.html
(後述)では、直接ソースコードに書き込みます。
Header:
ヘッダ名 | 値 |
---|---|
Authorization | Bearer アクセストークン |
Content-Type | application/json |
Body: {"xxxxx_name":"upate-nzCfxbP3"}
Body は、JSON 文字列です。{"列名":"値","列名":"値",...}
と書きます。
結果:
成功したら、204 No Content が返ってきます。
Dataverse で確認すると、Name 列が更新されています。
Get(テーブルデータ取得)で確認しても同様です。
その他:
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ではなく、レコード追加の動きに関しては、OData v4の仕様です。
画像アップロード
Method: Patch
URL: https://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)/xxxxx_image
xxxxx_image
は、画像型の列です。
Header:
ヘッダ名 | 値 |
---|---|
Authorization | Bearer アクセストークン |
x-ms-file-name | 画像ファイル名 |
Content-Type | application/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 が返ってきます。
Dataverse で確認すると、サムネイル画像が表示されます。
Get(テーブルデータ取得)で確認すると、image 列は、サムネイル画像(144×144)の Base64 文字列が取得されます。
その他:
一度に送信できるのは、最大 16MB で、それ以外の場合、チャンク(分割)して送信する必要があるような記述が見られましたが、約 30MB の画像をアップロードしても問題ありませんでした。(画像の最大サイズ=上限の 30720KB で列を作成しています。)
【https://learn.microsoft.com/ja-jp/power-apps/developer/data-platform/image-attributes
】16MB を超えるイメージに対してチャンク アップロードを使用するという制限がなくなりました。
とあります。
30MB 超の場合、さすがに、400 Bad Request
が返りました。
Body に null
を入れて再アップロードすると、画像が消えました。
Get Image(画像データダウンロード&表示)
Method: GET
URL: https://***********.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:
ヘッダ名 | 値 |
---|---|
Authorization | Bearer アクセストークン |
Content-Type | application/octet-stream |
Body: 無し
結果:
response.blob()
→ バイナリデータ → createObjectURL()
→ imgタグ
のurl=
に指定で、画像表示されます。
Delete(行削除)
Method: DELETE
URL: https://***********.api.crm*.dynamics.com/api/data/v9.2/xxxxx_gazos(d78b4***-****-****-****-*******279c8)
Header:
ヘッダ名 | 値 |
---|---|
Authorization | Bearer アクセストークン |
Body: 無し
結果:
成功したら、204 No Content が返ってきます。
レコードが消えました。
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 CRM → user_impersonation を アクセス許可の追加 済みとします。
Live Server
今回作成するのは、index.html
です。これを localhost:5500 の Web サーバーで見られるようにします。
今回は、Live Server
で実現します。
VSCode(Visual Studio Code)を起動して、拡張機能 Live Server
をインストールします。
設定 liveServer.settings.host
→ localhost
に変更します。
Web アプリ作成
Web アプリ といっても、index.html
一個だけです。index.html
のことを「アプリ」と言っています。
適当な場所に simplespa
(名称は任意) という空フォルダを作成し、ファイル → フォルダーを開く で選択します。
右クリック → 新しいファイル...
で 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 = ``;
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>
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 ボタンがあります。
これを押すだけで、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!
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。