WebフォームからSalesforce連携。レコード作成と画像アップロード。
今回やりたいこと
Webフォームでエンドユーザーからデータ入力(テキスト&画像)を受け付ける。
→ Salesforceへ入力データを送信。
テキスト情報からレコードを作成し、アップロードした画像と作成したレコードを紐づける。
※ 本記事は2023/10/04時点での筆者の知る範囲での情報の公開であり、再現性や結果を保証するものではありません。当記事の内容を応用する場合は、ご自身でもDYORの精神で情報収集することを推奨します。
APIの接続方法概要
OAuth認証を使用して接続します。
OAuth認証方法の一覧はこちらにありますので、詳細はそちらをご覧ください。
一般的なOAuth認証
- ユーザがアプリケーションを通してSalesforceのリソースにアクセスしようとすると、アプリケーションはユーザを認証サーバーのログインページに移動します(認証サーバーへのリダイレクト)。
ユーザは認証サーバーへログイン後、アプリケーションがSalesforceのリソースにアクセスすることを認証するかどうかを求められます。 - 1で認証されると、認証サーバーはユーザーを再びアプリケーションのページに案内します(アプリケーションへのリダイレクト)。
その際にアプリケーションは認証サーバーからSalesforceのリソースにアクセスするためのトークンを取得します(認証コードのステップを挟むなど詳細は認証方法によって変わります)。 - ユーザはアプリケーションで任意の操作(データ入力など)を行います。
- アプリケーションはアクセストークンを使用してSalesforcへ接続し、データのやりとり(ユーザの入力データを反映させるなど)を行います。
この仕組みにより、ユーザはアプリケーションに対して自身のログイン情報を直接共有せずに特定のアクセス許可を与えることができます。
また、エンドユーザがSalesforceのユーザであることが前提です。
Salesforceで上記の認証をするにはOAuth 2.0 Web サーバーフローを使うことになります。
その他にユーザ名パスワードフローがありますが、この方法はエンドユーザ情報をアプリケーションが知ることになるので非推奨のようです。
2023年夏からはデフォルト設定では無効になっていました。参考
今回行うOAuth認証、固定のユーザでログイン
今回はWebフォームを入力するエンドユーザーはSalesforceのユーザではない場合を想定するので、事前にSalesforceとの接続に使用するユーザを決めておき、そのユーザ情報をもとにアプリケーション側でSalesforceへアクセスします。
アプリケーションの中をさらに具体的に見ると、エンドユーザーの入力を受け付けるWebフォームをクライアントサイドで実装し、認証サーバーやSalesforceとの接続はサーバーサイド側で扱います。
サーバーサイドで認証関連を行うことで、ユーザ情報をクライアントサイドに公開しないようにします。
設定方法
Salesforceで上記の認証をするには、OAuth 2.0 クライアントログイン情報フローを使用します。
ちなみに今回はこちらから無料アカウントを作成し本番環境で挙動を確かめたので、Sandbox環境だと少し違いが出るかもしれません。
- API用のユーザ(インテグレーションユーザ)を作成します。
- プロファイルの作成。
[Salesforce API Only System Integrations]というプロファイルをコピーし新たにプロファイルを作成します。
作成したプロファイルを編集して、フォームから作成する任意のオブジェクトへの作成権限を有効化します。今回はサンプル(API参照名: Sample__c) というオブジェクトを作ることにします。 [Salesforce API Only System Integrations]はインテグレーションユーザの説明にもあるように、セキュリティの観点からAPIの操作権限を最小化しているものです。 - ユーザを作成し、ライセンスに[Salesforce Integration]を、プロファイルに先ほど作成したものを設定します。
- プロファイルの作成。
- 接続アプリケーション設定をリンク先に従って進めます。
特にOAuth設定の部分では以下がポイントです。
その他の認証方法には、OAuth 2.0 JWT ベアラーフローなどがあります。
画像データの送信方法
画像の扱い方についても先に触れます。
Salesforceで画像を扱うには[ファイル]と[メモ&ファイル]という二つの機能があり、ファイルはLightning Experienceで新しく追加されたものです。
そして、ファイルを操作するには以下のオブジェクトを利用します。
- CotentDocument.
ファイルをアップロードすると自動で作成されるもの。
ファイルそのもののメタデータや属性(例: タイトル、所有者など)を持ちますが、ファイルの実際の内容やバージョン情報は持っていません。 - ContentVersion.
ContentVersionはファイルの実際の内容(バイナリデータ)、そのバージョンの詳細、ファイルのタイプやサイズなどの情報を持っています。 - ContentDocumentLink.
ファイルと他のオブジェクトとのリンク情報を持ちます。
例えばSalesforceの画面上で、オブジェクトAのレコードページの関連リスト「ファイル」にファイルBをアップロードすると、次のステップが実行されます。
- アップロードされたファイルBの情報は
ContentVersion
オブジェクトに保存されます。 - ファイルがアップロードされると、関連する
ContentDocument
が自動的に作成されます。 - そして、このファイルがオブジェクトAのレコードに関連付けられるために、
ContentDocumentLink
レコードが作成されます。
このレコードのContentDocumentId
フィールドは、ファイルBのContentDocument
のIDに設定され、LinkedEntityId
フィールドはオブジェクトAのレコードIDに設定されます。
[このセクションの参考資料]
- Blob(Binary Large OBject)データの送信
- How to Upload a Document in Salesforce Files Using Rest API
- ContentVersionドキュメント
node.jsで簡単な実装
node.jsでサーバーを立ち上げ、実際にSalesforceにアクセスしてみます。
必要な情報
- クライアントキーとクライアントシークレット。
[設定] → [アプリケーションマネージャ]で先ほど作成した接続アプリケーションの[参照]を選択すると、以下のような[コンシューマの詳細を管理]というボタンがあるのでクリックします。 すると、[コンシューマ鍵]と[コンシューマの秘密]という二つの情報が表示されるので控えておきます。 - 環境のドメイン。
[設定] → [私のドメイン]を選択すると私のドメインのURLという情報が表示されるのでこちらも控えます。
コード
npm
もしくはyarn
を使用してモジュールを作成し、server.js
とsalesforceAPI
というファイルを作成します。
server.js
にサーバー処理、salesforceAPI
にsalesforceと連携用の関数を用意します。
必要なライブラリ等はimportを見て適宜インストールしてください。
server.js
app.get(...
の部分では、GETリクエスト時にhtmlを返却する関数を登録しています。
これによりクライアントサイドでWebフォームを形成します。
また、app.post(...
の部分では、WebフォームからPOSTされた情報をsubmitRequestToSalesforce
という関数を使用してsalesforceへ送信する関数を登録しています。
submitRequestToSalesforce
については次のセクションで扱います。
import { submitRequestToSalesforce } from "./salesforceAPI.js"; import express from "express"; // httpサーバーを作成するためのモジュール import multer, { memoryStorage } from "multer"; // multipart/form-dataをパースするためのモジュール(クライアントサイドからのファイルを受け取るために使う) const app = express(); const PORT = 3000; // ファイルをメモリ上に一時的に保存するためのmulterの設定 const upload = multer({ storage: memoryStorage() }); // クライアントサイドからのGETに対してhtmlを返す app.get("/", (req, res) => { res.send(` <form action="/" method="post" enctype="multipart/form-data"> <label for="Name">名前:</label><br> <input type="text" id="Name" name="Name" required><br><br> <label for="file1">ファイル1:</label> <input type="file" name="files"><br><br> <label for="file2">ファイル2:</label> <input type="file" name="files"><br><br> <input type="submit" value="Submit"> </form> `); }); // クライアントサイドからPOSTされた情報をSalesforceへ送信する app.post("/", upload.array("files", 2), async (req, res) => { try { const objectApiName = "Sample__c"; const requestObjectData = { Name: req.body.Name, }; const files = req.files; const success = await submitRequestToSalesforce( objectApiName, requestObjectData, files ); if (success) { res.send("Success"); } else { res.status(500).send("Error occurred while processing your request."); } } catch (error) { console.error(error); res.status(500).send(error.message); } }); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
node server.js
を実行し、http://localhost:3000/にアクセスすると以下のようにフォームが表示されます。
salesforceAPI.js
salesforceAPI.js
では、salesforceとの接続とsalesforceへのデータ送信の二つを行います。
初めに示すコードでは、(salesforceへのデータ送信を扱う)submitRequestToSalesforce
関数の中身は省略しています。
authenticateWithSalesforce
関数では、先ほど設定したクライアントログイン情報フローによる認証(salesforceとの接続)を行なっています。
認証リクエストの返り値のアクセストークンやインスタンスURLを、後ほどsalesforceへのデータ送信に利用します。
ここで、process.env.SF_CLIENT_ID
などの環境変数はdotenvパッケージを使用して.env
ファイルから取得しています。
テストであれば環境変数を使わず直接書き込んでも良いと思います。
# .envファイル SF_CLIENT_ID="事前に控えたコンシューマ鍵" SF_CLIENT_SECRET="事前に控えたコンシューマの秘密" SF_MY_DOMAIN="事前に控えた私のドメイン"
import dotenv from "dotenv"; // 環境変数を扱うためのモジュール import axios from "axios"; // HTTPリクエストを送信するためのモジュール import FormData from "form-data"; // multipart/form-dataにエンコーディングするためのモジュール(Salesforceへのファイル送信に使う) dotenv.config(); let sfAccessToken; // Salesforceから取得したアクセストークン let sfInstanceUrl; // SalesforceのインスタンスURL export const submitRequestToSalesforce = async ( objectApiName, requestObjectData, files ) => { if (!sfAccessToken || !sfInstanceUrl) { await authenticateWithSalesforce(); } ... }; const authenticateWithSalesforce = async () => { // OAuth 2.0 クライアントログイン情報フローを使用: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5 try { const response = await axios.post( `https://${process.env.SF_MY_DOMAIN}/services/oauth2/token`, null, { params: { grant_type: "client_credentials", client_id: process.env.SF_CLIENT_ID, client_secret: process.env.SF_CLIENT_SECRET, }, } ); sfAccessToken = response.data.access_token; sfInstanceUrl = response.data.instance_url; } catch (error) { if (error.response) { console.error("Auth error response:", error.response.data); } else { console.error("Auth request error:", error.message); } } };
次にsubmitRequestToSalesforce
についてです。
事前に整形されたデータがrequestObjectData
、files
として引数で渡ってきます。
初めに(const sfResponse = await axios.post( ...
の部分)アクセストークンやrequestObjectData
を使用してサンプルオブジェクトのレコードを作成します。
返り値からは作成したレコードのIdが取得できます。
次にファイルの数だけループ処理で以下を繰り返します。
ContentVersion
の作成- 1で自動で作成される
ContentDocument
から、レコードとのリンクに使用するidの取得 ContentDocumentLink
を作成して、作成したレコードとファイルを紐づける
export const submitRequestToSalesforce = async ( objectApiName, requestObjectData, files ) => { if (!sfAccessToken || !sfInstanceUrl) { await authenticateWithSalesforce(); } try { // Sampleの作成 const sfResponse = await axios.post( `${sfInstanceUrl}/services/data/v58.0/sobjects/${objectApiName}`, JSON.stringify(requestObjectData), { headers: { Authorization: `Bearer ${sfAccessToken}`, "Content-Type": "application/json", }, } ); const objectId = sfResponse.data.id; for (const file of files) { // ContentVersionの作成 const fileName = file.originalname; const fileContent = file.buffer; let formData = new FormData(); formData.append( "entity_content", JSON.stringify({ ReasonForChange: "Uploaded via form", PathOnClient: fileName, }), { contentType: "application/json", } ); formData.append("VersionData", fileContent, { filename: fileName, contentType: "application/octet-stream", }); const sfResponse2 = await axios.post( `${sfInstanceUrl}/services/data/v58.0/sobjects/ContentVersion`, formData, { headers: { Authorization: `Bearer ${sfAccessToken}`, ...formData.getHeaders(), }, } ); const contentVersionId = sfResponse2.data.id; // ContentVersionに関連するContentDocumentのIDを取得 const versionResponse = await axios.get( `${sfInstanceUrl}/services/data/v58.0/sobjects/ContentVersion/${contentVersionId}`, { headers: { Authorization: `Bearer ${sfAccessToken}`, }, } ); const contentDocumentId = versionResponse.data.ContentDocumentId; // ContentDocumentLinkオブジェクトを作成(ファイルをSample__cオブジェクトに関連付ける) const linkData = { ContentDocumentId: contentDocumentId, LinkedEntityId: objectId, ShareType: "V", }; await axios.post( `${sfInstanceUrl}/services/data/v58.0/sobjects/ContentDocumentLink`, linkData, { headers: { Authorization: `Bearer ${sfAccessToken}`, "Content-Type": "application/json", }, } ); } return true; } catch (error) { if (error.response) { console.error("REST API error response:", error.response.data); } else { console.error("REST API request error:", error.message); } return false; } };
これにより「レコードの作成 → ファイルのアプロード → レコードとファイルの紐付け」が実現できます。
ブラウザからWebフォームを提出すると、
salesforce画面上でレコードが作成されているので(リストを「すべて選択」にしないと見れません)、
中身を確認すると[関連]にファイルも紐づけられています。
[このセクションの参考資料]
まとめ
以上になります!
ちなみに、ここではファイルを扱うのにmulter
・FormData
・axios
等を使用してリクエストを作成しましたが、
別の機会(Next.js
でのアプリ構築)に同じようなリクエスト作成を再現しようとしても400エラー(Multipart message must include a non-binary part)が返ってきてしまいました。
ライブラリによる多少の違いでsalesforceが受け付けてくれないのかもしれません。
その時はこちらに記載されてるリクエストボディを参考にスクラッチでリクエストを作成しました。
メモ
- CURLによる接続方法
- JavaScriptにjsforceというライブラリがあり、簡単に操作できて便利そうです。