Shopify: JotFormでのユーザー登録フォーム作成 – API Gateway+Lambdaとwebhookの利用

Shopify

フォームの作成

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のコンテンツタイプに対して以下のテンプレートを設定します。 これにより、リクエストボディがBase64エンコードされてLambda関数に渡されます。
#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へデータを自動的に送信できます。設定手順は以下の通りです。

  1. フォームビルダーでフォームを開く
  2. 「設定」タブをクリック。
  3. 左側のメニューから「インテグレーション」を選択。
  4. インテグレーション一覧から「Webhooks」を検索して選択。
  5. 「Add WebHook」フィールドに、データを受け取るエンドポイントのURLを入力。
  6. 「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の対応は別途検討すること

参考

関連記事

カテゴリー

アーカイブ

Lang »