AEM Edge関数の開発

IMPORTANT
AEM Edge Functionsは現在ベータ版です。 機能やドキュメントは変更される可能性があります。 フィードバックについては、aemcs-edgecompute-feedback@adobe.comまでお問い合わせください。

目標は、AEM Edge関数を呼び出してサードパーティ APIから動的データを取得する動的なEdge Delivery Services ブロックを​ ビルド ​することです。

最初のステップは、2つのアップストリーム API呼び出しを1つのレスポンスに統合し、CORSをローカル開発に有効にするエンドポイントを公開するAEM Edge関数を開発することです。 リクエストとレスポンスの契約、エンドポイントの照合、およびアウトバウンド fetch()の基本については、Edge Functionsを使用したAPI エンドポイントの構築を参照してください。 ここでは、このチュートリアルに固有の部分のみを説明します。

まず定型文から始めます

AEM Edge Functions プロジェクト(Edge Delivery ServicesでのAEM Edge Functionsの設定で複製)には、2つのサンプルルート(/hello-world/weather)が付属しているため、初日にテストを行う作業が必要です。 どちらのチームも実際のプロジェクトに属しているわけではないので、まず定型文を確認し、必要ないものを削除しましょう。

API コントラクトの定義

ハンドラーを記述する前に、リクエストとレスポンスの形状を定義して、Edge Delivery Services ブロックやその他の呼び出し元を実装を読まずにコントラクトに対して構築できるようにします。

エンドポイント: GET /api/frescopa/estimated-delivery

GET /api/frescopa/estimated-delivery?sku=house-blend-medium-roast&postcode=90210

postcodeが必要です。 skuはオプションで、デフォルトはhouse-blend-medium-roastです。

応答本文:

応答が成功すると、次の形状が返されます。

// 200 OK
{
  "sku": "house-blend-medium-roast",
  "productName": "House Blend - Medium Roast",
  "inventoryStatus": "in-stock", // or "low-stock", "out-of-stock"
  "qtyLeft": 12,
  "fulfillmentRegion": "West Coast Fulfillment",
  "deliveryEta": "tomorrow",
  "cutoffMessage": "Order by 2:00 PM for same-day dispatch.",
  "message": "Arrives tomorrow in West Coast Fulfillment"
}

エラー応答がこの形状を共有しており、失敗の理由ごとに異なるcodeが使用されています。

// 400 missing postcode, 404 unknown sku, 405 wrong method: same shape, different code
{
  "error": "Missing required parameter",
  "code": "MISSING_POSTCODE", // or "UNKNOWN_SKU", "METHOD_NOT_ALLOWED"
  "message": "The postcode query parameter is required"
}

すべてのエラー本文にはcode フィールドが含まれているため、呼び出し元はmessageを解析する代わりに失敗の理由で分岐できます。 OPTIONS件のリクエストがCORS プリフライトに対して個別の応答を受け取ります。以下の​ ローカル開発に対するCORSの有効化で説明しています。

ビジネスロジックの実装

index.jsをルーティングに限定します。 ビジネスロジックは、エンドポイントごとに1つのフォルダーであるsrc/handlers/の下にあります。

追加するファイル

src/
├── index.js                              # routes to a handler, nothing else
├── handlers/estimated-delivery/
│   ├── handler.js                        # orchestrates the two upstream calls
│   ├── responses.js                      # builds the success and error JSON payloads
│   └── constants.js                      # route path and query-param defaults
├── lib/
│   ├── api-client.js                     # shared per-API token loading
│   └── cors.js                           # origin allow-list, applied to every response
└── mocks/                                # tutorial stand-ins for the two upstream APIs
    ├── catalog/product-api.js
    └── delivery/delivery-api.js

ハンドラー呼び出しとアップストリーム呼び出し

handler.jsのハンドラーはskupostcodeを受け取り、2つのアップストリームサービスを呼び出し、その結果を1つの応答にマージします。

// src/handlers/estimated-delivery/handler.js
async function estimatedDeliveryBySkuAndPostcodeHandler(req) {
  const url = new URL(req.url);
  const sku = (url.searchParams.get("sku") ?? DEFAULT_SKU).trim();
  const postcode = (url.searchParams.get("postcode") ?? "").trim();

  if (!postcode) {
    return missingPostcodeError(sku);
  }

  // API 1: catalog lookup
  const product = await getProductBySku(sku);
  if (!product) {
    return unknownSkuError(sku, postcode);
  }

  // API 2: fulfillment lookup
  const fulfillment = await getEstimatedDeliveryByPostcode(postcode);

  return json(buildSuccessResponse(product, fulfillment));
}

responses.jsはエラーと成功のJSON図形をハンドラーから保持し、constants.jsはルートパスとデフォルトのSKUを保持するので、handler.jsは2呼び出しシーケンスに集中し続けます。 src/mocks/の各モックは、データを返す前に、実際のAPI クライアントが使用するのと同じ形状の独自のトークンを読み込みます。

// src/mocks/catalog/product-api.js
export async function getProductBySku(sku) {
  await getApiToken(SECRETS.CATALOG);

  // Real API: uncomment and replace with your catalog endpoint.
  // const response = await authorizedFetch(
  //   `https://api.example.com/products?sku=${encodeURIComponent(sku)}`,
  //   SECRETS.CATALOG,
  // );
  // return response.ok ? response.json() : null;

  return PRODUCTS[sku] || null;
}

getApiToken()authorizedFetch()lib/api-client.jsに住んでいます。 product-api.jsdelivery-api.jsの両方がこのヘルパーを共有しているため、各アップストリーム APIは、認証ロジックを複製することなく、独自の秘密ストア キー(CATALOG_API_TOKENDELIVERY_API_TOKEN)を保持します。 シミュレートされた呼び出しを実際のAPIに置き換える準備ができたら、Edge Functionsで設定とシークレットを使用して、デプロイされたサイトでシークレット設定を行い、上記のブロックのコメントを解除します。

各プラットフォームでは、1回の呼び出しを​32回のアウトバウンドフェッチ呼び出しに制限しているため、ここで2回の呼び出しでは十分なヘッドルームが残ります。

ローカル開発のCORSを有効にする

Edge Delivery Services ブロックのローカル開発サーバーはhttp://localhost:3000で実行されます。 AEM Edge関数のローカル開発サーバーがhttp://127.0.0.1:7676で実行されています。 これらは2つの異なるオリジンです。そのため、CORS ヘッダーを使用しないと、コードがリクエストを見る前にブラウザーがブロックします。

cors.jsの許可リストに対するリクエストのオリジンを確認し、ブラウザーが最初に送信するOPTIONSのプリフライトリクエストに同じチェックを適用します。

// src/lib/cors.js
const ALLOWED_ORIGIN_EXACT = new Set([
  "http://localhost:3000",
  "http://127.0.0.1:3000",
]);

function isAllowedOrigin(origin) {
  return Boolean(origin) && ALLOWED_ORIGIN_EXACT.has(origin);
}

function applyCors(request, response) {
  const origin = request.headers.get("Origin");
  if (!isAllowedOrigin(origin)) {
    return response;
  }

  const headers = new Headers(response.headers);
  headers.set("access-control-allow-origin", origin);
  headers.set("vary", "Origin");
  return new Response(response.body, { status: response.status, headers });
}

function corsPreflightResponse(request) {
  const origin = request.headers.get("Origin");
  if (!isAllowedOrigin(origin)) {
    return new Response(null, { status: 403 });
  }

  return new Response(null, {
    status: 204,
    headers: {
      "access-control-allow-origin": origin,
      "access-control-allow-methods": "GET, OPTIONS",
      "access-control-allow-headers": "Content-Type",
      vary: "Origin",
    },
  });
}

export { applyCors, corsPreflightResponse };
// src/index.js
if (url.pathname === "/api/frescopa/estimated-delivery") {
  if (req.method === "OPTIONS") {
    finalResponse = corsPreflightResponse(req);
  } else if (req.method === "GET") {
    finalResponse = await estimatedDeliveryBySkuAndPostcodeHandler(req);
  }
}

finalResponse = applyCors(req, finalResponse);

参照の実装はALLOWED_ORIGIN_EXACTを正規表現で拡張して、デプロイされた開発、ステージ、および実稼動ドメインも許可します。これらのドメインへのリクエストはCDNを通じて同一生成元であり、技術的にはCORS ヘッダーは必要ありませんが、それらを照合すると、明示的に許可リストの自己文書化が行われます。 一度デプロイすると、ブラウザーは同一生成元リクエストのCORS チェックを送信しないため、余分なヘッダーは無害です。 CORS ヘルパーを配置したままにしておくこともできます。リクエストのオリジンが許可リストと一致する場合にのみヘッダーが追加されます。

fastly.tomlのローカル開発サーバーを設定します

fastly.tomlは、aio aem edge-functions serveによって開始されたローカル開発サーバーを構成します。 このチュートリアルでは、その[local_server.secret_stores] ブロックによって各アップストリーム APIにローカルトークンが与えられるので、ハンドラーは実際の資格情報を持たずにSecretStoreManager.getSecret()を呼び出すことができます。

# fastly.toml
[local_server.secret_stores]
  [[local_server.secret_stores.secret_default]]
    key = "CATALOG_API_TOKEN"
    data = "catalog-tutorial-token"
  [[local_server.secret_stores.secret_default]]
    key = "DELIVERY_API_TOKEN"
    data = "delivery-tutorial-token"

ボイラープレートでは、天気サンプルのOpen-Meteo APIの[local_server.backends] エントリも宣言されます。 このチュートリアルでは、実際のホストを呼び出す代わりに両方のアップストリーム呼び出しをシミュレートするので、そのエントリをリポイントするのではなく削除します。

[local_server] オプションの完全なセットについては、Fastly.toml リファレンス ​を参照してください。

エンドポイントをローカルでテストする

この時点では、API コントラクトのみをテストしています。エンドポイントのリクエストとレスポンスの形状は、後で呼び出されるEdge Delivery Services ブロックとは関係ありません。

Edge Delivery ServicesでAEM Edge Functionsを設定からローカル開発サーバーを起動します。

$ aio aem edge-functions serve

curlでハッピーパスを呼び出すか、ブラウザーでURLを開きます。

$ curl "http://127.0.0.1:7676/api/frescopa/estimated-delivery?sku=house-blend-medium-roast&postcode=90210"

応答が、カタログ検索からの製品名や、フルフィルメント検索からの配信見積もりなど、両方のアップストリーム呼び出しからのデータを同じJSON本文に結合することを確認します。

推定された配信エンドポイントに応答するローカル開発サーバー

次に、エラーパス responses.js ビルドを確認します。

# missing postcode
$ curl "http://127.0.0.1:7676/api/frescopa/estimated-delivery?sku=house-blend-medium-roast"

# unknown SKU
$ curl "http://127.0.0.1:7676/api/frescopa/estimated-delivery?sku=not-a-real-product&postcode=90210"

各ハンドラーは、汎用500ではなくcode フィールド (MISSING_POSTCODEUNKNOWN_SKU)を持つJSON エラー本文を返す必要があり、ハンドラーがアップストリーム呼び出しに到達する前に入力を検証することを確認します。

エンドポイントの追加

2つの設定ファイルがルートを宣言し、API エンドポイントの構築のパターンに従います。

config/edgeFunctions.yamlは、ボイラープレートとは変わらず、AEM Edge関数自体を宣言します。

# config/edgeFunctions.yaml
kind: "EdgeFunctions"
version: "1"
data:
  functions:
    - name: my-edge-function

config/cdn.yamlは、このチュートリアルのパスをその関数に転送するオリジン セレクターのルールを追加します。

# config/cdn.yaml
- name: route-estimated-delivery-to-edge-function
  when: { reqProperty: path, equals: "/api/frescopa/estimated-delivery" }
  action:
    type: selectAemOrigin
    originName: edgefunction-my-edge-function
    skipCache: true

参照実装の完全なファイルを参照してください:config/edgeFunctions.yamlおよびconfig/cdn.yaml

次の手順

Edge Delivery Services ブロックを開発するには、このエンドポイントを呼び出すブロックをスキャフォールドしてから、ユニバーサルエディターでオーサリングします。

その他のリソース

recommendation-more-help
experience-manager-learn-help-cloud-service