フォームの作成
1. Jotformでフォームを作成
- Jotformのアカウントにログインし、新しいフォームを作成します。
- ドラッグ&ドロップのインターフェースを使用して、必要なフィールドを追加し、デザインをカスタマイズします。
2. フォームの埋め込みコードを取得
- フォームビルダーの「公開」タブに移動します。
- 左側の「プラットフォーム」を選択し、一覧から「Shopify」を検索して選択します。
- 表示される埋め込みコードをコピーします。
3. Shopifyストアにフォームを埋め込む
- Shopifyの管理画面にログインし、「オンラインストア」>「ページ」に移動します。
- 既存のページを編集するか、新しいページを作成します。
- エディタをHTMLビューに切り替え、先ほどコピーした埋め込みコードを貼り付けます。
- 変更を保存し、ページを公開します。
API GatewayとLambdaによるAPI作成
Jotformから送信されるデータは、通常multipart/form-data
形式で送信されます。API Gatewayはデフォルトではこの形式を直接処理できないため、以下の対応が必要です。
API Gatewayでの設定
- API Gatewayでの設定: 「統合リクエスト」の「マッピングテンプレート」で、
multipart/form-data
のコンテンツタイプに対して以下のテンプレートを設定します。
#set($allParams = $input.params())
{
"body": "$util.base64Encode($input.body)",
"headers": {
#foreach($header in $allParams.header.keySet())
"$header": "$allParams.header.get($header)"#if($foreach.hasNext),#end
#end
},
"queryStringParameters": {
#foreach($queryParam in $allParams.querystring.keySet())
"$queryParam": "$allParams.querystring.get($queryParam)"#if($foreach.hasNext),#end
#end
}
}
Lambdaでの設定
- Lambda関数でのデコード: Lambda関数内で、受け取ったデータをデコードし、必要な情報を抽出します。このように設定することで、Jotformのフォーム送信データをAWSのサーバーレス環境でリアルタイムに処理することが可能になります。
import base64
def lambda_handler(event, context):
encoded_body = event['body_base64encoded']
decoded_body = base64.b64decode(encoded_body).decode('utf-8')
Webhookによる連携
4. Webhookの設定方法
Jotformは、フォーム送信データを外部サービスやアプリケーションと連携するためのWebhook機能とAPIを提供しています。これらを活用することで、リアルタイムなデータ連携やカスタムインテグレーションが可能となります。
Webhookを使用すると、フォーム送信時に指定したURLへデータを自動的に送信できます。設定手順は以下の通りです。
- フォームビルダーでフォームを開く。
- 「設定」タブをクリック。
- 左側のメニューから「インテグレーション」を選択。
- インテグレーション一覧から「Webhooks」を検索して選択。
- 「Add WebHook」フィールドに、データを受け取るエンドポイントのURLを入力。
- 「Complete Integration」をクリックして設定を保存。
これで、フォームが送信されるたびに指定したURLへデータが送信されます。
Lambda関数コードの紹介
import { Context, APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';
import { PrismaClient, meeting_state as meeting_state, meeting_type as meeting_type } from '@prisma/client';
import {
createOrUpdateMeeting,
CreateMeetingData,
getMeetingsByOrderIds,
getMeetingsByIds
} from './datasourceDB';
import {
postCustomer
} from './datasourceShopify';
import querystring from 'querystring';
import multiparty from 'multiparty';
import { Readable } from 'stream';
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
function parseMultipart(buffer: Buffer, contentType: string): Promise<any> {
return new Promise((resolve, reject) => {
const form = new multiparty.Form();
const req = new Readable();
req.push(buffer);
req.push(null);
req.headers = {
'content-type': contentType,
};
form.parse(req, (err: any, fields: any, files: any) => {
if (err) {
reject(err);
} else {
resolve({ fields, files });
}
});
});
}
function extractFieldsFromRawRequest(rawRequest: string[]): Record<string, any> {
if (!rawRequest || rawRequest.length === 0) {
throw new Error('rawRequest is empty or undefined');
}
// rawRequest配列の最初の要素をJSONとしてパース
const parsedRequest = JSON.parse(rawRequest[0]);
// 抽出対象のキー
const keysToExtract = ['q3_name', 'q4_email', 'q5_phoneNumber', 'q6_whichTopic', 'q7_message'];
// 必要なフィールドを抽出
const extracted: Record<string, any> = {};
keysToExtract.forEach((key) => {
if (parsedRequest[key]) {
extracted[key] = parsedRequest[key];
}
});
return extracted;
}
export const handlerJotFormWebhook = async (event: any, context: Context): Promise<any> => {
try {
const contentType = event.headers?.['content-type'] || event.headers?.['Content-Type'];
if (!contentType || !contentType.startsWith('multipart/form-data')) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid content type' }),
};
}
const bodyBuffer = Buffer.from(event.body, 'base64');
const { fields, files } = await parseMultipart(bodyBuffer, contentType);
const extractedFields = extractFieldsFromRawRequest(fields.rawRequest)
const first_name = extractedFields['q3_name']?.['first'] || '';
const last_name = extractedFields['q3_name']?.['last'] || '';
const email = extractedFields['q4_email'] || '';
const phone = extractedFields['q5_phoneNumber']?.['full'] || '';
const topics = extractedFields['q6_whichTopic'] || [];
const message = extractedFields['q7_message'] || '';
const topic = Object.values(topics).join('|')
const secretName = process.env.SECRET_NAME_DB_CRED;
const secret = await secretsManager.getSecretValue({ SecretId: secretName as string }).promise();
const dbCredentials = JSON.parse(secret.SecretString ?? '{}');
const databaseUrl = `postgresql://${dbCredentials.username}:${dbCredentials.password}@${process.env.DATASOURCE_URL}:5432/${process.env.DB_NAME}?schema=public`;
const prisma = new PrismaClient({
datasources: {
db: {
url: databaseUrl,
},
},
});
let responseObject = {}
const customerData = {
first_name: first_name,
last_name: last_name,
phone: phone,
email: email,
topic: topic,
message: message
};
const shopifyResponse = await postCustomer(customerData);
responseObject = { customer: shopifyResponse };
return {
statusCode: 200,
body: JSON.stringify({
body: responseObject
}),
};
} catch (error) {
console.error("Error: " + error.message);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};
※ SQL Injectionの対応は別途検討すること