ミッション
Salesforceから、HULFT Squareのスクリプトを実行する
こんにちは、あるいはこんばんは、バッタもんです
今回はSalesforceの画面にリンクを作成し、HULFT Squareのスクリプトを動かしてみました
たやすくできると始めたところが、多くの敵に遭遇したので攻略法を書き残します
(貴方がウo〇〇〇スの〇昏さんなら、必要のない知識ですのでスルーしてください)
なお、スクショの一部をマスキングしていますので、ご了承ください
HULFT Square(サービス側)の設定
HULFT SquareのAPIマネジメントは、プログラミング知識がなくても、データ連携のためのAPIを簡単に作成・公開できます
具体的には、以下の3つの手順で設定が完了します
- HTTPメソッドと実行スクリプトの紐づけ(APIプロジェクト)
- アクセス権限の設定(APIクライアント)
- エンドポイントの公開(REST APIジョブ)
詳しくは、以下をご覧ください
Salesforce(クライアント側)の設定
この記事の内容を実践される場合は(サポートが充実している)本番ライセンスのクローンsandboxがオススメですが、デベロッパーライセンスでもOKです
(無料トライアルはApexクラス非対応なのでNG)
Bearer認証を使用してAPIを呼び出す
上記実現のために、フローとApexクラスを利用します
開始当初はフローだけで済ませたいと考えていましたが、Bearer認証を採用するHULFT Squareの場合、SalesforceのHTTPコールアウトで必要な外部ログイン情報(指定ログイン情報)のカスタムヘッダーを可変で扱うことが難しく断念
Try&Errorの結果、フロー+Apexクラスが最も現実的と判断しました
なお、JavaScriptだけでも実現できる可能性がありますが、下記理由から除外しました
JavaScriptの直接記述は(classic互換で簡単にできちゃうけど)非推奨!
Salesforceは、ボタンやリンクに直接JavaScriptを書けますが、classic互換の機能です
ボタン(リンク)の設定画面の動作でJavaScriptを選択すると、非推奨のメッセージが表示されます
※詳しくは、以下のTrailheadサイトをご参照ください
Lightning の活用および JavaScript ボタンからの移行
https://trailhead.salesforce.com/ja/content/learn/modules/lex_javascript_button_migration/lex_javascript_button_migration_intro
Apexクラスの作成
以下の3つのApexクラスを作成します(テキストエディタで編集後、Salesforceに登録)
01. Login APIでアクセストークンを取得する
02. アクセストークンを更新する
03. REST APIジョブのエンドポイントを実行する
Apexコードの作成は、Google Geminiさんにお願いしました
ChatGPTと比較してApexの生成の精度が高く、デバッグに協力的だったのが理由ですが、推しポイントは、プロンプトにURLリンクが貼れることです
それぞれのApexコードを掲載します
01. Login APIでアクセストークンを取得する
Apexクラス:HulftSquareLoginService
/**
* @description HULFT SquareのLogin APIを呼び出し、アクセストークンを取得します。
*/
public class HulftSquareLoginService {
/**
* @description Flowから呼び出し可能なLogin API実行メソッド。
* @param requests ログイン情報を含むリクエストのリスト。通常は単一のリクエスト。
* @return 処理結果(アクセストークン、成功フラグ、メッセージ)のリスト。
*/
@InvocableMethod(label='HULFT Square Login API (アクセストークン取得)' description='HULFT Square Login APIを呼び出し、アクセストークンを取得します。')
public static List<HulftSquareAuthResponse> getAccessToken(List<HulftSquareAuthRequest> requests) {
List<HulftSquareAuthResponse> results = new List<HulftSquareAuthResponse>();
if (requests == null || requests.isEmpty()) {
results.add(new HulftSquareAuthResponse(false, null, null, 'リクエストデータがありません。', 400));
return results;
}
HulftSquareAuthRequest request = requests[0]; // フローからの呼び出しは通常単一のリクエストを想定
try {
HttpRequest req = new HttpRequest();
req.setEndpoint(request.loginUrl); // HULFT Square Login APIのエンドポイントURL
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody('{"email": "' + request.username + '", "password": "' + request.password + '"}');
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String accessToken = (String) responseMap.get('accessToken'); // HULFT Squareのキー名に合わせる
String refreshToken = (String) responseMap.get('refreshToken'); // 必要であればリフレッシュトークンも取得
results.add(new HulftSquareAuthResponse(true, accessToken, refreshToken, 'アクセストークンが正常に取得されました。', res.getStatusCode()));
} else {
results.add(new HulftSquareAuthResponse(false, null, null, 'Login APIの実行に失敗しました。ステータスコード: ' + res.getStatusCode() + ', レスポンス: ' + res.getBody(), res.getStatusCode()));
}
} catch (Exception e) {
results.add(new HulftSquareAuthResponse(false, null, null, 'エラーが発生しました: ' + e.getMessage(), 500));
}
return results;
}
/**
* @description フローからの入力パラメータを定義する内部クラス。
*/
public class HulftSquareAuthRequest {
@InvocableVariable(label='Login API URL' description='HULFT Square Login APIのエンドポイントURL')
public String loginUrl;
@InvocableVariable(label='Username' description='HULFT Squareのログインに使用するユーザー名(メールアドレス)')
public String username;
@InvocableVariable(label='Password' description='HULFT Squareのログインに使用するパスワード')
public String password;
}
/**
* @description フローへの出力結果を定義する内部クラス。
* アクセストークン、リフレッシュトークン、成功フラグ、メッセージを返します。
*/
public class HulftSquareAuthResponse {
@InvocableVariable(label='成功' description='APIコールが成功したか')
public Boolean isSuccess;
@InvocableVariable(label='アクセストークン' description='取得したアクセストークン')
public String accessToken;
@InvocableVariable(label='リフレッシュトークン' description='取得したリフレッシュトークン')
public String refreshToken;
@InvocableVariable(label='メッセージ' description='APIコール結果に関するメッセージ')
public String message;
@InvocableVariable(label='ステータスコード' description='HTTPレスポンスのステータスコード')
public Integer statusCode;
public HulftSquareAuthResponse(Boolean isSuccess, String accessToken, String refreshToken, String message, Integer statusCode) {
this.isSuccess = isSuccess;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.message = message;
this.statusCode = statusCode;
}
}
}
02. アクセストークンを更新する
Apexクラス:HulftSquareTokenRefreshService
/**
* @description HULFT Squareのアクセストークン更新APIを呼び出し、アクセストークンを更新します。
* 認証ヘッダーに現在のアクセストークンを使用するように修正。
*/
public class HulftSquareTokenRefreshService {
/**
* @description Flowから呼び出し可能なアクセストークン更新メソッド。
* @param requests リフレッシュトークン情報を含むリクエストのリスト。通常は単一のリクエスト。
* @return 処理結果(新しいアクセストークン、成功フラグ、メッセージ)のリスト。
*/
@InvocableMethod(label='HULFT Square アクセストークン更新' description='HULFT Squareのアクセストークン更新APIを呼び出し、新しいアクセストークンを取得します。認証ヘッダーに現在のアクセストークンを使用します。')
public static List<HulftSquareAuthResponse> refreshAccessToken(List<HulftSquareRefreshRequest> requests) {
List<HulftSquareAuthResponse> results = new List<HulftSquareAuthResponse>();
if (requests == null || requests.isEmpty()) {
results.add(new HulftSquareAuthResponse(false, null, null, 'リクエストデータがありません。', 400));
return results;
}
HulftSquareRefreshRequest request = requests[0]; // フローからの呼び出しは通常単一のリクエストを想定
try {
HttpRequest req = new HttpRequest();
req.setEndpoint(request.refreshUrl); // HULFT Squareアクセストークン更新APIのエンドポイントURL
req.setMethod('PUT'); // HULFT SquareのAPI仕様に合わせる(例: PUT)
req.setHeader('Content-Type', 'application/json');
// 既存のアクセストークンを認証ヘッダーとして追加
if (request.currentAccessToken != null) {
req.setHeader('Authorization', 'Bearer ' + request.currentAccessToken);
}
req.setBody('{"refreshToken": "' + request.refreshToken + '"}'); // HULFT SquareのJSON形式に合わせる
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String newAccessToken = (String) responseMap.get('accessToken'); // HULFT Squareのキー名に合わせる
String newRefreshToken = (String) responseMap.get('refreshToken'); // 必要であれば新しいリフレッシュトークンも取得
results.add(new HulftSquareAuthResponse(true, newAccessToken, newRefreshToken, 'アクセストークンが正常に更新されました。', res.getStatusCode()));
} else {
results.add(new HulftSquareAuthResponse(false, null, null, 'アクセストークンの更新に失敗しました。ステータスコード: ' + res.getStatusCode() + ', レスポンス: ' + res.getBody(), res.getStatusCode()));
}
} catch (Exception e) {
results.add(new HulftSquareAuthResponse(false, null, null, 'エラーが発生しました: ' + e.getMessage(), 500));
}
return results;
}
/**
* @description フローからの入力パラメータを定義する内部クラス。
* 現在のアクセストークンを認証ヘッダー用に含めます。
*/
public class HulftSquareRefreshRequest {
@InvocableVariable(label='Refresh API URL' description='HULFT Squareアクセストークン更新APIのエンドポイントURL')
public String refreshUrl;
@InvocableVariable(label='リフレッシュトークン' description='更新に使用するリフレッシュトークン')
public String refreshToken;
// 新しい入力変数
@InvocableVariable(label='現在のアクセストークン' description='認証ヘッダーとして使用する現在のアクセストークン (Login APIで取得したもの)')
public String currentAccessToken;
}
/**
* @description フローへの出力結果を定義する内部クラス。
* アクセストークン、リフレッシュトークン、成功フラグ、メッセージを返します。
*/
public class HulftSquareAuthResponse {
@InvocableVariable(label='成功' description='APIコールが成功したか')
public Boolean isSuccess;
@InvocableVariable(label='アクセストークン' description='取得したアクセストークン')
public String accessToken;
@InvocableVariable(label='リフレッシュトークン' description='取得したリフレッシュトークン')
public String refreshToken;
@InvocableVariable(label='メッセージ' description='APIコール結果に関するメッセージ')
public String message;
@InvocableVariable(label='ステータスコード' description='HTTPレスポンスのステータスコード')
public Integer statusCode;
public HulftSquareAuthResponse(Boolean isSuccess, String accessToken, String refreshToken, String message, Integer statusCode) {
this.isSuccess = isSuccess;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.message = message;
this.statusCode = statusCode;
}
}
}
03. REST APIジョブのエンドポイントを実行する
Apexクラス:HulftSquareJobExecutor
/**
* @description HULFT SquareのREST APIジョブのエンドポイントを実行します。
* アクセストークンのチェックとデバッグログを実装しています。
*/
public class HulftSquareJobExecutor {
/**
* @description Flowから呼び出し可能なREST APIジョブ実行メソッド。
* @param requests ジョブ実行情報を含むリクエストのリスト。通常は単一のリクエスト。
* @return 処理結果(レスポンスボディ、成功フラグ、メッセージ、ステータスコード)のリスト。
*/
@InvocableMethod(label='HULFT Square REST APIジョブ実行' description='HULft Square REST APIジョブのエンドポイントを実行します。')
public static List<HulftSquareJobResponse> executeRestApiJob(List<HulftSquareJobRequest> requests) {
List<HulftSquareJobResponse> results = new List<HulftSquareJobResponse>();
// 実行ユーザーの情報をデバッグログに出力
// System.debug('--- HulftSquareJobExecutor Start ---');
// System.debug('実行ユーザーID: ' + UserInfo.getUserId());
// System.debug('実行ユーザー名: ' + UserInfo.getUserName());
// System.debug('実行ユーザーメール: ' + UserInfo.getUserEmail());
if (requests == null || requests.isEmpty()) {
results.add(new HulftSquareJobResponse(false, null, 'リクエストデータがありません。', 400));
return results;
}
HulftSquareJobRequest request = requests[0]; // フローからの呼び出しは通常単一のリクエストを想定
// デバッグログで入力値を確認
// System.debug('HulftSquareJobExecutor - Job Endpoint URL: ' + request.jobEndpointUrl);
// System.debug('HulftSquareJobExecutor - HTTP Method: ' + request.httpMethod);
// System.debug('HulftSquareJobExecutor - Request Body (raw): ' + request.requestBody); // 修正後の確認のため、元の値をログ出力
// アクセストークンがNullまたは空でないかを確認
if (String.isBlank(request.accessToken)) {
// System.debug('HulftSquareJobExecutor - ERROR: Access Token is null or blank.');
results.add(new HulftSquareJobResponse(false, null, 'エラー: アクセストークンが提供されていません。', 400));
return results;
}
// System.debug('HulftSquareJobExecutor - Access Token (first 10 chars): ' + request.accessToken.substring(0, Math.min(request.accessToken.length(), 10)) + '...');
try {
HttpRequest req = new HttpRequest();
req.setEndpoint(request.jobEndpointUrl);
String method = (request.httpMethod != null && !String.isBlank(request.httpMethod)) ? request.httpMethod.toUpperCase() : 'POST';
req.setMethod(method);
req.setHeader('Authorization', 'Bearer ' + request.accessToken.trim());
// System.debug('HulftSquareJobExecutor - Authorization Header: Bearer ' + request.accessToken.trim().substring(0, Math.min(request.accessToken.trim().length(), 10)) + '...');
req.setHeader('Content-Type', 'application/json');
// ★ポイント: request.requestBody が null の場合は setBody を呼び出さない
if (request.requestBody != null) {
req.setBody(request.requestBody);
System.debug('HulftSquareJobExecutor - Request Body sent: ' + req.getBody());
} else {
System.debug('HulftSquareJobExecutor - Request Body: Not set (null).');
}
Http http = new Http();
HttpResponse res = http.send(req);
System.debug('HulftSquareJobExecutor - Response Status Code: ' + res.getStatusCode());
System.debug('HulftSquareJobExecutor - Response Body: ' + res.getBody());
if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
results.add(new HulftSquareJobResponse(true, res.getBody(), 'REST APIジョブが正常に実行されました。', res.getStatusCode()));
} else {
results.add(new HulftSquareJobResponse(false, res.getBody(), 'REST APIジョブの実行に失敗しました。ステータスコード: ' + res.getStatusCode(), res.getStatusCode()));
}
} catch (Exception e) {
System.debug('HulftSquareJobExecutor - EXCEPTION: ' + e.getMessage() + ' at line ' + e.getLineNumber());
results.add(new HulftSquareJobResponse(false, null, 'エラーが発生しました: ' + e.getMessage(), 500));
}
System.debug('--- HulftSquareJobExecutor End ---');
return results;
}
// HulftSquareJobRequest および HulftSquareJobResponse クラスの定義
public class HulftSquareJobRequest {
@InvocableVariable(label='Job Endpoint URL' description='HULFT Square REST APIジョブの完全なエンドポイントURL')
public String jobEndpointUrl;
@InvocableVariable(label='アクセストークン' description='認証に使用するアクセストークン')
public String accessToken;
@InvocableVariable(label='HTTPメソッド' description='ジョブ実行に使用するHTTPメソッド(GET, POST, PUT, DELETEなど)。デフォルトはPOST。')
public String httpMethod;
@InvocableVariable(label='リクエストボディ' description='ジョブ実行に含めるリクエストボディ(JSON形式の文字列)')
public String requestBody;
}
public class HulftSquareJobResponse {
@InvocableVariable(label='成功' description='APIコールが成功したか')
public Boolean isSuccess;
@InvocableVariable(label='レスポンスボディ' description='APIからの生のレスポンスボディ')
public String responseBody;
@InvocableVariable(label='メッセージ' description='APIコール結果に関するメッセージ')
public String message;
@InvocableVariable(label='ステータスコード' description='HTTPレスポンスのステータスコード')
public Integer statusCode;
public HulftSquareJobResponse(Boolean isSuccess, String responseBody, String message, Integer statusCode) {
this.isSuccess = isSuccess;
this.responseBody = responseBody;
this.message = message;
this.statusCode = statusCode;
}
}
}
// ★ポイント: request.requestBody が null の場合は setBody を呼び出さない |
---|
※コードの中ほどにある上記コメントですが、生成直後は、GetなどRequest Bodyが必要ない場合でも"{ }"が出力される仕様でした
この仕様でリクエストを送信すると、HULFT Squareでは403 Forbeddenを返すのでご注意を!
sandboxにログインし設定画面へ
ユーザ名とパスワードでログイン
右上の歯車マークから設定を選択
設定画面が表示される
Apexクラスをsandboxに登録
Apexクラスを選択し、(一覧上部の小さな)新規ボタンを選択
作成したApexコード(ここでは、HulftSquareLoginService)を...
まるっと貼り付けてsaveボタンを選択(Apexクラス名はコードから自動設定)
HulftSquareLoginServiceを登録しました
フローの作成
3つのApexクラスを順に処理するフローを作成
01. Login APIでアクセストークンを取得する
02. アクセストークンを更新する
03. REST APIジョブのエンドポイントを実行する
フロー:HSQApiService
実行時に必要な値を変数に代入後、上記01~03を順次実行し、レスポンスを画面に表示
順次実行について
このフローは、01~03を連続実行しますが、アクセストークンの有効期限を考慮した設計で、APIコール数を最適化できます
要素:Variable1(変数割り当て)
ログインユーザ(メールアドレス)、パスワード、リフレッシュトークン、エンドポイントをフロー変数に代入します
秘匿性の高い情報の扱いについて
ここではフローに直接値を設定していますが、パスワードやリフレッシュトークンなどの秘匿性の高い情報は、セキュリティ維持のためフローに直接設定せず、カスタムメタデータ型の保護されたパッケージとして保管して、参照するフロー自体のセキュリティ(誰がフローを実行できるか)にも十分注意して設計してください
要素:HULFTSquareLoginAPI(Apexアクション)
要素:HULFTSquareTokenRefresh(Apexアクション)
要素:HULFTSquareJobExecute(Apexアクション)
REST APIジョブのエンドポイントを実行(HTTPメソッドがGETの場合)
DisplayResponse(画面表示)
デバッグ実行
フローが完成したのでデバッグボタンで実行(デバッグオプションは初期値)
無事正常終了(レスポンスは、スクリプトの実行IDと処理結果)
カスタムリンク
作成したフローを画面から起動するためにカスタムリンクを作成します
対象のオブジェクト(今回は取引先)を検索して選択
以下を設定後、保存ボタンを選択
表示ラベル:HULFT Squareリクエスト
名前:hulft_square_request
説明:(任意)
表示種別:詳細ページリンク
動作:新規ウィンドウに表示
コンテンツリソース:URL
コンテンツ:/flow/HsqApiService(呼び出すフローの名前)
カスタムリンクHULFT Squareリクエストが登録できました
ページレイアウト
左側のメニューからページレイアウトを選択し、Account Layoutを選択
カスタムリンクを選択肢、HULFT Squareリクエストを、画面下方のカスタムリンク
(ヘダーを表示しない) にドラッグ&ドロップ、保存ボタンを選択して登録
取引先画面で任意の取引先を選択し、説明欄の下にHULFT Squareリクエストの表示を確認
リモートサイトの設定
新規リモートサイトボタンを選択し、HULFT Squareエンドポイントのサイトアドレスを追加
・ログインAPI用:https://app.square.hulft.com
・ジョブ実行API用:https://<自社ドメイン名>.square.hulft.com
リンクからフローを実行
取引先画面のHULFT Squareリクエストリンクを選択しHULFT Squareにリクエストを送信
実行結果
HULFT Squareから返されるメッセージ、レスポンスボディ、ステータスコードを画面に表示
無事レスポンスを受け取ることが出来ました
おわりに
最後までお読みいただき、ありがとうございます
今回は、クライアントをSalesforceに限定した実現方法をご紹介しましたが、
HULFT SquareのAPIをコールできるクライアント(サービス)は多く存在します
今後も様々なクライアントから、HULFT Squareのスクリプトを動かしてみたいと思います
ではまた、いつか、どこかで...
バッタもん