この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
クラスメソッド営業統括本部 CRMチーム 進地 です。 Salesforceに保持しているWebサイトURLからドメインを抜き出したいという要望がありましたので、諸々調査してみました!
やりたいこと
- AWS CDKを使ってAPI GatewayにREST APIを作成。このAPIはURLをパラメータで受け取って、そのドメインを抽出して返す。
- Salesforceにて取引先を作成するタイミングで1で作ったAPIをコールして、結果を受け取る。コールする時に「取引先.Webサイト(API参照名: Account.Website)」の値をパラメータとしてAPIに渡す。
- 受け取った結果をSalesforceの取引先に保存する。
このエントリーでは2までを実施してみます(3は結構やっかい。後日別エントリーで補完できるかな?)
やってみた
AWS CDKを使ってAPI GatewayにREST APIを作成する
AWS CDK Intro WorkshopのAPI Gatewayのページを参考にREST APIを作りました。
まず、CDKプロジェクトを新規に作ります。ここではプロジェクト名をdomain-extractor
としました。
$ cdk init domain-extractor
$ cd domain-extractor
次に、リソースを定義します。言語にはTypeScriptを選びました。lib/domain-extractor-stack.ts
を次のように書きました。
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import { ServicePrincipal } from 'aws-cdk-lib/aws-iam';
export class DomainExtractorStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const domainExtractor = new lambda.Function(this, 'DomainExtractorHandler', {
runtime: lambda.Runtime.NODEJS_16_X,
code: new lambda.AssetCode('lambda'),
handler: 'domainExtractor.handler'
});
domainExtractor.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));
const api = new apigw.LambdaRestApi(this, 'domainExtractor', {
handler: domainExtractor,
proxy: false,
deployOptions: {
loggingLevel: apigw.MethodLoggingLevel.INFO,
dataTraceEnabled: true,
metricsEnabled: true,
}
});
const extractor = api.root.addResource('extractor');
extractor.addMethod('POST');
}
}
Lambdaファンクションをまず定義し、次にそのLambdaファンクションを呼び出すAPI Gatewayを定義しています。
なお、
domainExtractor.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com'));
の指定がないと、API Gatewayから定義したLambdaが呼び出せずPermissionエラーになります(ここは結構ハマった)。
[apigateway] grant lambda invoke permission when new stages are added #3983
次に、Lambdaファンクションの本体(DomainExtractorHandler
)を作成します。
$ cd lambda
lambdaディレクトリに移動して、domainExtractor.js
を次のように書きました。
const url = require('url');
const psl = require('psl');
exports.handler = async function(event) {
console.log("request:", JSON.stringify(event, undefined, 2));
const body = JSON.parse(event.body);
const parsed = psl.parse(url.parse(body.url).hostname);
const returnValue = {
domain: parsed.domain
};
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(returnValue, null, 2)
};
};
urlモジュールを使ってパラメータで受け取ったURLのホスト名(hostname)を抽出し、それをpslモジュールのparseメソッドに渡すことでドメインを抽出しています。
pslモジュールはlambdaディレクトリ直下でnpm install
する必要があることに注意が必要です。
$ pwd
domain-extractor/lambda
$ npm install psl
あとは、デプロイして、
$ cdk deploy
:
DomainExtractorStack.domainExtractorEndpointEC1F9D14 = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
作成したAPIの動作確認をしてみます。
$ curl -X POST -d '{"url": "https://dev.classmethod.jp/"}' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/extractor
:
{
"domain": "classmethod.jp"
}
期待通りドメインが返りました。
Salesforceにて取引先を作成するタイミングで1で作ったAPIをコールして、結果を受け取る
手前味噌ですが、私が過去に書いた次のエントリーを参照して作成したAPIをコールします。
Salesforceでの指定ログイン情報の作成は、参照先記事の方法で実施済みとします(名前をAWS_APIGW
として定義したものとします)。
取引先を作成するタイミングで実行したいので、取引先(Account)に対するApex Triggerを作成します。
triggers/AccountTrigger.trigger
を次のように実装してみました。
trigger AccountTrigger on Account (before insert) {
new AccountTriggerHandler().execute();
}
処理の実体はAccountTriggerHandler
というApexクラスの方で行います。
public with sharing class AccountTriggerHandler extends TriggerHandler {
/**
* 作成前処理
* @param newList 作成した取引先
*/
public override void beforeInsert(List<Sobject> newList) {
// Webサイトの値を取り出し、APIをコールする
for (Account acc : (List<Account>)newList) {
if (acc.Website != null) {
DomainExtractorCalloutAsync.execute('{"url": "' + acc.Website + '"}');
}
}
}
}
TriggerHandler
はTrigger作成用の汎用抽象クラスです。作っておくと便利ですよ。
public abstract class TriggerHandler {
public void execute() {
switch on Trigger.operationType {
when BEFORE_INSERT { this.beforeInsert(Trigger.new); }
when BEFORE_UPDATE { this.beforeUpdate(Trigger.oldMap, Trigger.newMap); }
when BEFORE_DELETE { this.beforeDelete(Trigger.oldMap); }
when AFTER_INSERT { this.afterInsert(Trigger.newMap); }
when AFTER_UPDATE { this.afterUpdate(Trigger.oldMap, Trigger.newMap); }
when AFTER_DELETE { this.afterDelete(Trigger.oldMap); }
when AFTER_UNDELETE { this.afterUndelete(Trigger.newMap); }
}
}
protected virtual void beforeInsert(List<Sobject> newList) {}
protected virtual void beforeUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
protected virtual void beforeDelete(Map<Id, Sobject> oldMap) {}
protected virtual void afterInsert(Map<Id, Sobject> newMap) {}
protected virtual void afterUpdate(Map<Id, Sobject> oldMap, Map<Id, Sobject> newMap) {}
protected virtual void afterDelete(Map<Id, Sobject> oldMap) {}
protected virtual void afterUndelete(Map<Id, Sobject> newMap) {}
}
AccountTriggerHandler
の中から、実際のAPIのコールアウト処理はさらにDomainExtractorCalloutAsync
クラスで行っています。
global with sharing class DomainExtractorCalloutAsync implements Database.AllowsCallouts {
@future(callout=true)
public static void execute(String body) {
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:AWS_APIGW/prod/extractor');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept', 'application/json');
req.setBody(body);
HttpResponse res = http.send(req);
CalloutResponse cres = (CalloutResponse)JSON.deserializeStrict(res.getBody(), CalloutResponse.class);
System.debug(cres.domain);
}
class CalloutResponse {
String domain;
}
}
Salesforceで取引先を新規作成してみて、開発者コンソールのログにAPIの実行結果(取引先のWebサイトから抽出したドメイン)が記載されていることを確認します。ここでは取引先のWebサイトにhttps://dev.classmethod.jp/を指定してみました。
期待する結果(classmethod.jp
)が取れていますね。