Salesforce REST APIをcurlで試す

参考 - https://qiita.com/shunkosa/items/ec5fab901401c364ee7a

環境 - Windows 11 (curlコマンドはコマンドプロンプトにて実行)

## 接続アプリケーション作成

### 設定＞アプリケーションマネージャーにて新規接続アプリケーション作成
・OAuth設定の有効化にチェック
・コールバックURLは適当でいいそうなので「https://XXX」
・選択したOAuth範囲はフルアクセス
・クライアントログイン情報フローを有効化にチェック

### 当該接続アプリケーションの[Manage]→[ポリシーを編集]
・「タイムアウト値」でアクセストークンの使用無しタイムアウト時間を設定 ※使うと延命されるような気がするが気のせいかも
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112363/2671e13c-6239-7cf5-ceaa-02a548a10f56.png)
・今回はクライアントログイン情報フローの「別のユーザーとして実行」をシステム管理者にしておく (対象オブジェクトの対象項目はオブジェクトマネージャの項目レベルセキュリティの設定にてシステム管理者から参照可能としておく)

### 当該接続アプリケーションの[参照]→[コンシューマーの詳細を管理]
・メールされた確認コードを入力してコンシューマー鍵(クライアントID)・コンシューマーの秘密(クライアントシークレット)を得る


## アクセストークン取得 ###

### 必要な情報
・ドメイン - 使用しているSalesforce組織のURLのドメイン部分 (「https://」の後の「XXX.sandbox.lightning.force.com」等)
・クライアントID - 先述
・クライアントシークレット - 先述

### curlコマンド
```
curl -v "https://＜ドメイン＞/services/oauth2/token?grant_type=client_credentials&client_id=＜クライアントID＞&client_secret=＜クライアントシークレット＞"
```

### 正常実行結果(抜粋)
```
GET /services/oauth2/token?grant_type=client_credentials&client_id=＜クライアントID＞&client_secret=＜クライアントシークレット＞ HTTP/1.1
Host: ＜ドメイン＞
```

```
HTTP/1.1 200 OK

{"access_token":"＜アクセストークン＞","instance_url":"https://＜ドメイン＞","id":"https://test.salesforce.com/id/XXX/XXX","token_type":"Bearer","issued_at":"1707715933703"}
```

### 異常実行結果(抜粋)

#### クライアントID違い
```
HTTP/1.1 400 Bad Request

{"error":"invalid_client_id","error_description":"client identifier invalid"}
```

#### クライアントシークレット違い
```
HTTP/1.1 400 Bad Request

{"error":"invalid_client","error_description":"invalid client credentials"}
```

#### ドメイン違い
```
Could not resolve host: ＜ドメイン＞
```

#### URL違い
```
HTTP/1.1 404 Not Found

(エラー画面HTML)
```

## SELECT

### 必要な情報
・ドメイン - 先述
・アクセストークン - 先述
・APIバージョン - 「v59.0」等
・SOQL - 以下、例
```
SELECT Id FROM Account WHERE CreatedDate >= 2024-01-08T15:34:10.000+0900 LIMIT 2
```

### curlコマンド
```
curl -v "https://＜ドメイン＞/services/data/v59.0/query/?q=SELECT%20Id%20FROM%20Account%20WHERE%20CreatedDate%20%3E%3D%202024-01-08T15%3A34%3A10.000%2B0900%20LIMIT%202" -H "Authorization: Bearer ＜アクセストークン＞"
```
「%20」は「+」でも可

### 正常実行結果(抜粋)
```
GET /services/data/v59.0/query/?q=SELECT%20Id%20FROM%20Account%20WHERE%20CreatedDate%20%3E%3D%202024-01-08T15%3A34%3A10.000%2B0900%20LIMIT%202 HTTP/1.1
Host: ＜ドメイン＞
Authorization: Bearer ＜アクセストークン＞
```

```
HTTP/1.1 200 OK

{"totalSize":2,"done":true,"records":[{"attributes":{"type":"Account","url":"/services/data/v59.0/sobjects/Account/XXX"},"Id":"XXX"},{"attributes":{"type":"Account","url":"/services/data/v59.0/sobjects/Account/XXX"},"Id":"XXX"}]}
```

レスポンスボディ整形結果
```
{
  "totalSize": 2,
  "done": true,
  "records": [
    {
      "attributes": {
        "type": "Account",
        "url": "/services/data/v59.0/sobjects/Account/XXX"
      },
      "Id": "XXX"
    },
    {
      "attributes": {
        "type": "Account",
        "url": "/services/data/v59.0/sobjects/Account/XXX"
      },
      "Id": "XXX"
    }
  ]
}
```

### 異常実行結果(抜粋)

#### 0件
```
HTTP/1.1 200 OK

{"totalSize":0,"done":true,"records":[]}
```

#### オブジェクト名違い
```
HTTP/1.1 400 Bad Request

[{"message":"\nSELECT Id FROM XXX WHERE CreatedDate >= 2024-01-08T15:34:10.000+0900\n               ^\nERROR at Row:1:Column:16\nsObject type 'XXX' is not supported. If you are attempting to use a custom object, be sure to append the '__c' after the entity name. Please reference your WSDL or the describe call for the appropriate names.","errorCode":"INVALID_TYPE"}]
```

#### 項目名違い
```
HTTP/1.1 400 Bad Request

[{"message":"\nSELECT XXX FROM Account WHERE CreatedDate\n       ^\nERROR at Row:1:Column:8\nNo such column 'XXX' on entity 'Account'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.","errorCode":"INVALID_FIELD"}]
```

#### アクセストークンタイムアウト
```
HTTP/1.1 401 Unauthorized

[{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}]
```

### 件数が多い場合 (試していない)
https://developer.salesforce.com/docs/atlas.ja-jp.api_rest.meta/api_rest/dome_query.htm
検索結果に残りある場合、「nextRecordsUrl」が返されるので、次のリクエストでそこにリクエストを発行して続きを受け取る

## INSERT

### 必要な情報
・ドメイン - 先述
・アクセストークン - 先述
・APIバージョン - 先述
・オブジェクト名 - Account等
・JSONファイル - 以下、例
```
{
  "XXX__c" : "xxx",
  "XXX__c" : "＜値1＞;＜値2＞", ★複数選択リスト
  "XXX__c" : "2001-01-01" ★日付
}
```

### curlコマンド
```
curl --trace-ascii - "https://＜ドメイン＞/services/data/v59.0/sobjects/Account" -H "Authorization: Bearer ＜アクセストークン＞" -H "Content-Type: application/json" -d @＜JSONファイル名＞"
```

### 正常実行結果(抜粋)
```
POST /services/data/v59.0/sobjects/Account HTTP/1.1
Host: ＜ドメイン＞
Authorization: Bearer ＜アクセストークン＞
Content-Type: application/json
Content-Length: ＜リクエストボディサイズ＞

＜JSONファイルの内容＞ ★改行が削除されていたりする
```

```
HTTP/1.1 201 Created

{"id":"XXX","success":true,"errors":[]}
```

### 異常実行結果(抜粋)

#### ユニーク制約エラー
```
HTTP/1.1 400 Bad Request

[{"message":"重複値が見つかりました: XXX__cの値が重複しているレコードの ID: XXX","errorCode":"DUPLICATE_VALUE","fields":[]}]
```

#### 必須エラー
```
HTTP/1.1 400 Bad Request

[{"message":"値を入力してください: [XXX__c]","errorCode":"REQUIRED_FIELD_MISSING","fields":["XXX__c"]}]
```

#### オブジェクト名間違い
```
HTTP/1.1 400 Bad Request

[{"errorCode":"NOT_FOUND","message":"The requested resource does not exist"}]
```

#### 項目名間違い
```
HTTP/1.1 400 Bad Request

[{"message":"No such column 'XXX__c' on sobject of type Account","errorCode":"INVALID_FIELD"}]
```

#### 桁数制限エラー
```
HTTP/1.1 400 Bad Request

[{"message":"＜XXX__cの表示ラベル＞: データ値が大きすぎる: 123456789012345678901234567890123 (max length=32)","errorCode":"STRING_TOO_LONG","fields":["XXX__c"]}]
```

#### 日付形式エラー
```
HTTP/1.1 400 Bad Request

[{"message":"VALUE_STRING 値 2001-01-XX から date のインスタンスを並列化できないか、要求に必須項目がない可能性があります at [line:1, column:170]","errorCode":"JSON_PARSER_ERROR"}]
```

#### 選択リスト値エラー
```
HTTP/1.1 400 Bad Request

[{"message":"＜XXX__cの表示ラベル＞: 制限つき選択リスト項目の値が不適切: XXX","errorCode":"INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST","fields":["XXX__c"]}]
```

## 複数件UPSERT

対象オブジェクトにId以外の外部キーを作っておくのが業務上必要になったりする
(要求元はSalesforceのIdなんか持ってないので会員番号等の業務上のキーをUPSERT時のキーにしたい等)

https://developer.salesforce.com/docs/atlas.ja-jp.api_rest.meta/api_rest/resources_composite_sobjects_collections_upsert.htm によれば最大200件まで

### 必要な情報
・ドメイン - 先述
・アクセストークン - 先述
・APIバージョン - 先述
・オブジェクト名 - Account等
・外部キー項目名 - XXXNumber__c等
・JSONファイル - 以下、例
```
{
  "allOrNone" : false, ★エラーレコードあっても正常レコードは入れる
  "records": [
    {
      "attributes" : {"type" : "Account"},
      "XXXNumber__c": "xxx1",
      "XXX__c": "xxx"
    },
    {
      "attributes" : {"type" : "Account"},
      "XXXNumber__c": "xxx2",
      "XXX__c": "xxx"
    }
  ]
}
```

### curlコマンド
```
curl --trace-ascii - -X PATCH "https://＜ドメイン＞/services/data/v59.0/composite/sobjects/Account/XXXNumber__c" -H "Authorization: Bearer ＜アクセストークン＞" -H "Content-Type: application/json" -d @＜JSONファイル名＞"
```

### 正常実行結果(抜粋)
```
PATCH /services/data/v59.0/composite/sobjects/Account/XXXNumber__c HTTP/1.1
Host: ＜ドメイン＞
Authorization: Bearer ＜アクセストークン＞
Content-Type: application/json
Content-Length: ＜リクエストボディサイズ＞

＜JSONファイルの内容＞ ★改行が削除されていたりする
```

```
HTTP/1.1 200 OK

[{"id":"XXX","success":true,"errors":[],"created":false},{"id":"XXX","success":true,"errors":[],"created":true}]
```

レスポンスボディ整形結果
```
[
  {
    "id": "XXX",
    "success": true,
    "errors": [],
    "created": false ★UPDATE
  },
  {
    "id": "XXX",
    "success": true,
    "errors": [],
    "created": true ★INSERT
  }
]
```

### 異常実行結果(抜粋)

HTTPステータスコードが200であったりするので注意

#### attributesなし
```
HTTP/1.1 200 OK

[{"success":false,"errors":[{"statusCode":"JSON_PARSER_ERROR","message":"Nested object for polymorphic foreign key must have an attributes field before any other fields.","fields":[]}],"created":false},{"success":false,"errors":[{"statusCode":"JSON_PARSER_ERROR","message":"Nested object for polymorphic foreign key must have an attributes field before any other fields.","fields":[]}],"created":false}]
```

#### 日付形式エラー
```
HTTP/1.1 200 OK

[{"success":false,"errors":[{"statusCode":"JSON_PARSER_ERROR","message":"VALUE_STRING 値 2001-01-xx から date のインスタンスを並列化できないか、要求に必須項目がない可能性があります at [Source: N/A; line: -1, column: -1][line:-1, column:-1]","fields":[]}],"created":false},{"id":"XXX","success":true,"errors":[],"created":false}]
```

#### 選択リスト値エラー
```
HTTP/1.1 200 OK

[{"id":"XXX","success":false,"errors":[{"statusCode":"INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST","message":"＜XXX__cの表示ラベル＞: 制限つき選択リスト項目の値が不適切: 999z","fields":["XXX__c"]}],"created":false},{"id":"XXX","success":true,"errors":[],"created":false}]
```

#### 必須エラー
```
HTTP/1.1 200 OK

[{"id":"XXX","success":false,"errors":[{"statusCode":"REQUIRED_FIELD_MISSING","message":"値を入力してください: [XXX__c]","fields":["XXX__c"]}],"created":false},{"id":"XXX","success":true,"errors":[],"created":false}]
```

#### 項目名違い
```
HTTP/1.1 200 OK

[{"success":false,"errors":[{"statusCode":"INVALID_FIELD","message":"No such column 'XXX__c' on sobject of type Account","fields":[]}],"created":false},{"id":"XXX","success":true,"errors":[],"created":false}]
```

#### 桁数制限エラー
```
HTTP/1.1 200 OK

[{"id":"XXX","success":false,"errors":[{"statusCode":"STRING_TOO_LONG","message":"＜XXX__cの表示ラベル＞: データ値が大きすぎる: 123456789012345678901234567890123 (max length=32)","fields":["XXX__c"]}],"created":false},{"id":"XXX","success":true,"errors":[],"created":false}]
```

#### 1ファイル内で外部キーが重複
```
HTTP/1.1 200 OK

[{"success":false,"errors":[{"statusCode":"DUPLICATE_VALUE","message":"Duplicate external id specified: xxx","fields":["XXXNumber__c"]}],"created":false},{"success":false,"errors":[{"statusCode":"DUPLICATE_VALUE","message":"Duplicate external id specified: xxx","fields":["XXXNumber__c"]}],"created":false}]
```

#### アクセストークンタイムアウト
```
HTTP/1.1 401 Unauthorized
[{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}]
```