Microsoft DataverseのテーブルをPowerShellからCRUD操作してみた

## 概要

DataverseのテーブルをPowerShellを使用してWeb APIでCRUD操作します。Power AppsでDataverseにデータを蓄積し、Power Automate for desktopでオンプレ基幹にデータを入力したいと思い試しました。Mac版PowerShellを使用していますがWindowsでも可能です。

## 環境

- macOS Monterey 12.1
- MacBook Pro (13-inch, M1, 2020)
- PowerShell 7.2.1
- Visual Studio Code

## 注意

- AzureAD（組織アカウント）が必要です。
- アプリ作成、セキュリティロール設定可能な権限が必要です。
- 認可グラントは[クライアントクレデンシャルフロー](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow)を使います。
- データの機密度や社内規定などの諸条件によって認可グラントは変更してください。
- [Microsoft 365開発者プログラム](https://developer.microsoft.com/ja-jp/microsoft-365/dev-program)および[Power Apps開発者向けプラン](https://powerapps.microsoft.com/ja-jp/developerplan/)で試しています。
- 自己責任でお願いいたします
- 2021年12月の情報です.

## macOSへのPowerShellインストールについて

<https://docs.microsoft.com/ja-jp/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.2>

homebrewからインストールして使用しました。
インストーラー版は試していません。

## 作業の目次

1. Power Apps
   - Dataverseにテーブルを作成
2. AzureAD
   - アプリ登録
   - アプリケーション（クライアント）IDの取得
   - ディレクトリ（テナント）IDの取得
   - クライアントシークレットの取得
   - APIアクセス許可の追加
3. Power Platform管理センター
   - APIエンドポイントの確認
   - セキュリティロールの追加
   - アプリケーションユーザーの追加とロール設定
4. PowerSellスクリプトの作成
   - 読込操作
   - 作成操作
   - 更新操作
   - 削除操作

## 1.　Power Apps

Dataverseでテーブル作成
使用する環境のDataverseにテーブルを作成しサンプルデータを用意します。
![create_table](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/80aec873-690c-840b-ea46-f90c7af90123.jpeg)

今回はプライマリ列Name、数値列valueを作成し、サンプルデータとしました。
![add_data](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/46bf79a7-b341-e594-1399-54a329bb5312.png)


## 2.　AzureAD

- AzureADでアプリ登録をします。
![create_app01](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/95fa3254-5f73-173b-c6a7-8580cbf899f8.jpeg)

![create_app02](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/1e4507e2-dd18-bc90-e6ca-a36f11b6fcba.jpeg)


- 概要に戻り、アプリケーション（クライアント）IDとディレクトリ（テナント）IDを取得。
![ClientID](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/d4344be5-a689-f246-316e-ee154c903254.jpeg)


- 「証明書とシークレット」に移動し有効期限を選択して追加します。
![ClientSecret](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/c1d4fe22-5b69-d185-b2bd-5d19c941740d.jpeg)

追加したクライアントシークレットをコピーしてテキストエディットなどに控えておきます。後から参照することはできません。わからなくなってしまった場合は再取得します。
![ClientSecret2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/ed214dc4-dc60-e873-e603-7f4ac839b53b.jpeg)

- APIのアクセス許可の「アクセス許可の追加」からDynamics CRMを追加し、「user_impersonation」にチェックします。
![Scope](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/e3924fe8-4668-a71a-874d-3e99c980c07d.jpeg)


## 3.　Power Platform管理センター

- APIエンドポイントを取得
  Dataverseのテーブルがある環境設定を開き「リソース」を展開し、「すべてのレガシ設定」にアクセスします。
  ![Endpoint01](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/4275e860-7783-74a4-f834-24fe92e95c64.jpeg)

  Dynamics365が開いたら設定を展開し「カスタマイズ」に進みます。
  ![Endpoint02](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/12efea20-9d60-67c3-722f-f21a0f07b198.jpeg)

「開発者リソース」に進みます
  ![Endpoint03](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/33a634bd-b496-e7b8-31f6-3daa4bb40471.jpeg)

  インスタンスのWeb APIにエンドポイントが表示されているので控えておきます。
  ![Endpoint04](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/cd33e9ef-f71e-4fe2-dc28-2955aa02d62f.jpeg)

- セキュリティロールの追加
  設定＞セキュリティロールに移動して「新しいロール」を作成します。
  ![Role](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/8825a93e-8551-c258-07e9-7b1f98fa64f3.jpeg)

  ロール名を設定し、ユーザー定義エンティティタブに移動します。
  ![Role](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/edcaa688-bd8f-eef6-bcb7-472d7f2d84e1.jpeg)

  作成したテーブルを探し「作成」「読み込み」「書き込み」「削除」の権限をセットし保存します。
  ![Role](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/bf4ddd5d-9a35-5227-c445-787ac4e34b72.jpeg)

  ![Role](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/10d2df2e-3e67-920a-e1b7-4e317d3eb452.jpeg)


- アプリケーションユーザーの追加とロール設定
  環境に戻りS2Sアプリをすべて表示します。
  ![appuser](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/e2fb27f9-aef5-323a-703b-c5bfc0e41b3e.jpeg)

  アプリ追加にAzureADでアプリ登録した名前が出てくるので選択します。
  ![appuser](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/e6f95283-c9fd-5a0f-4c83-6e7156107835.jpeg)

  環境タブに現在の環境を選択し、セキュリティロールに作成したロールを割り当て保存します。
  ![appuser](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/b46d9022-330c-7fc4-8071-1f24b871d0ba.jpeg)

## 4. PowerSellスクリプトの作成

### 読込操作

#### アクセストークンの取得
   - 認可グラントはクライアントクレデンシャルで行います。
   - OAuth2.0トークンエンドポイントはv1を使用します。
   - $resourceAppIdUriはDataverseにテーブルを作成したPower Platform環境です。

```ps1:PowerShell
$tenantId = '********-****-****-****-************' # your tenant ID
$appId = '********-****-****-****-************' # your app ID
$appSecret = '**************************************' # your app secret
$resourceAppIdUri = 'https://***********.crm*.dynamics.com' #your env

$tokenEndpoint= "https://login.microsoftonline.com/$tenantId/oauth2/token"

$body = @{
  resource      = "$resourceAppIdUri"
  client_id     = "$appId"
  client_secret = "$appSecret"
  grant_type    = "client_credentials"
}

$response = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $body -ErrorAction Stop
$token = $response.access_token
Write-Output $token #test output
```

#### クエリの作成および実行

作成したテーブルの列名からクエリを構成します。
   データの作成日時 (createdon)でフィルターをかけてみます。
    ![sampletable](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/3455d7c9-4d7b-d304-5699-44a21d16d848.jpeg)

   - ODataを使用したクエリ
        <https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/webapi/query-data-web-api>

日付の形式に注意が必要です。「2021/12/16 14:34:00」のような形式はエラーになります。この例では「2021-12-16T14:34:00Z」とします。

```ps1:PowerShell
$query = '$select=cr1ee_name,cr1ee_value,cr1ee_sampletableid,createdon,modifiedon&$filter=(createdon gt 2021-12-16T14:34:00Z)'
```

   - FetchXmlを使用したクエリ
      <https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/use-fetchxml-construct-query>
        nullを含むプロパティは省かれてしまうので注意が必要です。[Docs](https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/webapi/retrieve-and-execute-predefined-queries)から引用

:::note warn
OData構文を使用するクエリとは異なり、事前定義されたクエリまたはfetchXmlから返されるデータは、null値を持つプロパティを返しません。 値がnullの場合、プロパティは結果に含まれません。
:::

```ps1:PowerShell
$query = 'fetchXml=<fetch mapping="logical">
  <entity name="cr1ee_sampletable">
    <attribute name="cr1ee_name" />
    <attribute name="cr1ee_value" />
    <attribute name="cr1ee_sampletableid" />
    <attribute name="createdon" />
    <attribute name="modifiedon" />
    <filter>
      <condition attribute="createdon" operator="gt" value="2021/12/17 14:33:59" />
    </filter>
  </entity>
</fetch>'
```

#### クエリの実行

環境のAPIエンドポイントにSchemaNameではなく[「LogicalCollectionName」](https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/entity-metadata#table-names)＋「？」とし、GETメソッドでクエリをリクエストします。LogicalCollectionNameはシステムで生成されるテーブル名の複数形です。困ったことに、正しく複数形になるとは限らないのが最大のハマりポイントでした。たとえばholidayはholidaysではなくholidaiesになっていました。[Metadata Tools](https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/browse-your-metadata)を使用して確認したほうが無難です。
    ![metadatatools](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/9b38d17c-35ed-54a9-3ace-e379a965553e.jpeg)

```ps1:PowerShell
$headers = @{Authorization = " Bearer $Token" }
$endPoint='https://***********.api.crm7.dynamics.com/api/data/v9.2/cr1ee_sampletables' #Your API Endpoint/LogicalCollectionName
$result = Invoke-RestMethod -Uri $endPoint"?$query" -Method 'GET' -Headers $headers

Write-Output $result
```

### データの整形
   - JSONで出力

    ```ps1:PowerShell
     Write-Output $result.value | ConvertTo-Json
    ```

    ![json](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/a38d0770-8b05-1d32-8867-bb33f2481f13.jpeg)

   - テーブル形式で表示
   
```ps1:PowerShell
$data = 0..($result.value.count - 1) | ForEach-Object {
  [PSCustomObject]@{ 
    RowID  = $result.value."cr1ee_sampletableid"[$_]
    Name  = $result.value."cr1ee_name"[$_]
    Value = $result.value."cr1ee_value"[$_] 
    作成日   = $result.value."createdon"[$_]
    修正日   = $result.value."modifiedon"[$_]
  } 
}

Write-Output $data | Format-Table -AutoSize
```

![table](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/7b6b9110-eee3-39f0-8eff-96c845366341.jpeg)

   - CSVに出力
   
```PS1:PowerShell
$data| Export-Csv -path "/Users/ユーザー名/パス/ファイル名.csv" -Encoding UTF8 -NoTypeInformation #Your save file path
```

![csv](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680039/069e2583-f5ff-a1c1-b184-55d612434a57.jpeg)


### 作成操作

APIエンドポイント/LogicalCollectionNameにJSON形式でPOSTリクエストします。

```PS1:PowerShell
$headers = @{Authorization = " Bearer $Token" }
$endPoint = 'https://*******.api.crm7.dynamics.com/api/data/v9.2/cr1ee_sampletables'#Your API Endpoint/LogicalCollectionName

$body = '{"cr1ee_name":"XYZ","cr1ee_value":385'

Invoke-RestMethod -Uri $endPoint -Method 'POST' -Headers $headers -Body $body -ContentType "application/json"
```

### 更新操作

APIエンドポイント/LogicalCollectionName(RowID)にJSON形式でPATCHリクエストします。

```ps1:PowerShell
$endPoint = 'https://*******.api.crm7.dynamics.com/api/data/v9.2/cr1ee_sampletables'#Your API Endpoint/LogicalCollectionName
$headers = @{Authorization = " Bearer $Token" }
$rowId = '*******-****-****-****-***********' #Patch RowID
$body = '{"cr1ee_name":"MKJ","cr1ee_value":676}'

Invoke-RestMethod -Uri $uri"($rowId)" -Method 'PATCH' -Headers $headers -Body $body -ContentType "application/json"
```

### 削除操作

APIエンドポイント/LogicalCollectionName(RowID)にDELETEメソッドをリクエストします。

```ps1:PowerShell
$endPoint = 'https://*******.api.crm7.dynamics.com/api/data/v9.2/cr1ee_sampletables'#Your API Endpoint/LogicalCollectionName
$headers = @{Authorization = " Bearer $Token" }
$rowId = '*******-****-****-****-***********' #Delete RowID

Invoke-RestMethod -Uri $uri"($rowId)" -Method 'DELETE' -Headers $headers -Body $body
```

## まとめ

Power Automate for desktopの12月アップデートでDataverseのアクションが追加される予定でしたが、今回は見送られたようです。そこで我慢できず😔PowerShellを使用してDataverseのテーブルをWeb APIからCRUD操作しました。今回はMac版PowerShellから試しましたがとくに不具合はありませんでした。Power Automateのクラウドコネクター経由するよりレスポンスがよく、Power Automate for desktopから呼び出してもストレスなさそうです。Dataverseのこと以外にも認可フローやロール設定など学習範囲は広いですがいろいろ応用出来そうです。

## 参考

<https://docs.microsoft.com/ja-jp/powerapps/developer/data-platform/>

<https://docs.microsoft.com/ja-jp/azure/active-directory/develop/>

<https://docs.microsoft.com/ja-jp/powershell/scripting/learn/deep-dives/everything-about-pscustomobject?view=powershell-7.2>

<https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/format-table?view=powershell-7.2>