mcpField
C
commerce-app-storage

commerce-app-storage

github.com/adobe数据与数据库316269 次安装今天更新
代码仓库

关于 commerce-app-storage

此技能用于将 App Builder Database Storage 集成到现有 Adobe Commerce 应用,面向开发者提供数据库配置、运行时操作和安装步骤的指导。

主要功能

  1. 01配置工作区数据库
  2. 02Web 与事件操作模式
  3. 03文档增删查改
  4. 04安装时创建集合和索引
  5. 05认证与区域配置指导

适用场景

  1. 01将 Commerce 应用数据存入数据库文档
  2. 02通过数据库操作处理 HTTP 请求
  3. 03处理 Commerce 事件或 Webhook
  4. 04在应用安装时准备集合和索引

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/adobe/skills --skill commerce-app-storage

安装后在 Claude Code 中输入/commerce-app-storage确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/
db-action.ts
setup-database.ts
evals/
evals.json

详细介绍

为 Commerce 应用添加数据库存储

将 App Builder Database Storage 集成到现有 Commerce 应用,并生成一个使用 @adobe/aio-lib-db 读取和写入文档的运行时操作。该库类似 MongoDB:数据存储在文档集合中,并可使用熟悉的筛选条件进行查询。

无论操作类型如何,数据库访问代码都完全相同;不同之处在于操作的注册方式以及处理器的返回内容:

  • Web 操作——可通过 HTTP 调用(web: "yes");使用 @adobe/aio-commerce-lib-core 中的 responses 辅助函数构建并返回响应。
  • 事件/Webhook 操作——由 Commerce 事件或 Webhook 调用;通过 commerce-app-eventing(runtimeActions)或 commerce-app-webhooks(runtimeAction)在 app.commerce.config.ts 中引用。

前置条件

  • 确认应用已生成脚手架并完成初始化,不能仅确认配置文件存在。必须同时满足以下两项:
    • 项目根目录中存在 app.commerce.config.ts,并且
    • 项目已初始化——这可通过已生成的 src/commerce-extensibility-1/ 目录和已安装的 node_modules(@adobe/aio-commerce-lib-app 依赖)来确认。
  • 如果缺少 app.commerce.config.ts,请停止操作,先调用 commerce-app-init(它会写入配置文件,然后运行初始化)。
  • 如果配置文件存在但项目尚未初始化(没有 src/commerce-extensibility-1/ 或 node_modules),请先运行 npx @adobe/aio-commerce-lib-app init 再继续。初始化是幂等的——它会找到现有配置,跳过交互式提示,安装依赖并生成项目文件。
  • 只有在项目已配置 TypeScript 构建环境(webpack-config.cjs + 根目录下的 tsconfig.json)后,才能使用 TypeScript 编写操作和自定义安装脚本;对于 TypeScript Commerce 配置,该环境由 init 生成——参见 commerce-app-init。否则,请使用 JavaScript 编写。
  • 必须在 Adobe Developer Console 中将 App Builder Data Services API(API 代码 AppBuilderDataServicesSDK)添加到项目中——所有使用数据库的工作区都必须添加(除 App Builder 外无需特殊许可证)。如果未添加,运行时操作将无法通过数据库服务的身份验证。

步骤 1 — 配置工作区数据库

AIO 项目工作区与工作区数据库之间严格一一对应。建议在 app.config.yaml 中以声明式方式配置:执行 aio app deploy 时,如果数据库尚不存在,就会自动配置:

application:
  runtimeManifest:
    database:
      auto-provision: true
      region: emea # amer | apac | emea | aus — the single source of truth for the region

仅包含扩展的应用需要变通方案。 由于 aio app CLI 插件中的一个 bug(并非 aio-lib-db 的问题),只有当 application 运行时清单中至少有一个包含运行时操作的包时,aio app deploy 才会执行声明式自动配置。纯粹由 extensions 构建的应用(提交指南推荐的布局)没有 application 操作,因此部署时会悄悄跳过配置。添加一个空的 packages 映射,以及一个用于创建配置步骤所需目录的 post-app-build 钩子,让 application 块“足够完整”,以便运行配置:

application:
  hooks:
    post-app-build: "mkdir -p dist/application/actions" # provisioning expects this dir to exist
  runtimeManifest:
    packages: {} # empty map — required by the config schema so the application block validates with no actions
    database:
      auto-provision: true
      region: emea # single source of truth — see the region callout below

对于本地开发,在 aio app run / aio app dev 期间不会执行声明式自动配置。请先使用 CLI 后备方案配置一次(自助操作,无需特殊权限)。只有安装了 存储 CLI 插件后,才能使用 aio app db … 命令:

aio plugins install @adobe/aio-cli-plugin-app-storage
aio app db provision --region <amer|apac|emea|aus>

区域是唯一数据源。 清单 database 块中的 region 必须与每次 initDb({ region }) 调用(或 AIO_DB_REGION)传入的 region 一致——每个操作以及安装步骤(步骤 6)都必须如此。不一致会导致连接失败。更改区域具有破坏性:运行 aio app db delete,更新清单中的 database.region,然后重新配置。

步骤 2 — 安装库

npm install @adobe/aio-lib-db

步骤 3 — 明确意图

向用户收集以下信息:

  • 操作类型:Web 操作或事件/Webhook 操作(参见上文的两种结构)。
  • 集合名称和所需的操作(插入/查找/更新/删除)。
  • 区域:必须与清单中的 database.region 匹配(参见步骤 1 中的区域说明)。将其传递给 init(),或设置 AIO_DB_REGION。

步骤 4 — 注册操作

将操作添加到 src/commerce-extensibility-1/ext.config.yaml 中用户定义的程序包(名称可任意指定,但保留名称 app-management 除外)。

**每个数据库操作都必须设置 include-ims-credentials: true。**如果不设置,aio-lib-db 就没有用于身份验证的 IMS 令牌,运行时连接会失败(如果操作在安装期间运行,应用安装也会失败)。请勿省略此注解。

# src/commerce-extensibility-1/ext.config.yaml
runtimeManifest:
  packages:
    app-management:
      # ... auto-generated — do not edit
    my-app: # any name except "app-management"
      actions:
        store-record:
          function: actions/store-record/index.js # relative to src/commerce-extensibility-1/
          runtime: nodejs:24
          web: "yes" # "yes" for a web action; "no" for an event/webhook action
          annotations:
            include-ims-credentials: true # REQUIRED for aio-lib-db auth
字段限制
程序包名称小写字母数字字符和连字符;不得使用保留名称 app-management
function相对于 src/commerce-extensibility-1/ 的路径——不能是 src/... 或相对于根目录的路径
include-ims-credentials必须为 true——否则 init() 没有 IMS 令牌,连接会失败
web可通过 HTTP 调用的 Web 操作为 "yes";事件/Webhook 处理程序为 "no"
集合名称非空字符串;如果集合不存在,会在首次写入时创建
区域必须与清单中的 database.region 匹配(amer | apac | emea | aus)

步骤 5 — 实现处理程序

每个处理程序都遵循相同的生命周期:解析 IMS 身份验证 → 生成令牌 → init → connect → 使用集合 → 始终在 finally 中调用 close。

// Web action — src/commerce-extensibility-1/actions/store-record/index.ts
import { buildErrorResponse, ok } from "@adobe/aio-commerce-lib-core/responses";
import {
  getImsAuthProvider,
  resolveImsAuthParams,
} from "@adobe/aio-commerce-lib-auth";
import { init as initDb } from "@adobe/aio-lib-db";

export async function main(params: Record<string, unknown>) {
  let client;
  try {
    // Resolve the injected AIO_COMMERCE_AUTH_IMS_* params, then mint a raw token string.
    const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
    const token = await authProvider.getAccessToken();
    const db = await initDb({ token, region: "emea" }); // must match the manifest database.region
    client = await db.connect();
    const records = client.collection("records");

    const result = await records.insertOne({
      ...(params.document as object),
      createdAt: new Date().toISOString(),
    });
    return ok({ body: { result } });
  } catch (error: any) {
    return buildErrorResponse(error.statusCode || 500, {
      body: { message: error.message },
    });
  } finally {
    if (client) await client.close(); // always close — avoids connection leaks
  }
}

事件和 Webhook 操作都通过 web: "no" 注册,但它们的负载结构不同——请勿将二者视为相同:

  • 事件处理程序(通过 commerce-app-eventing 接线):提取的字段位于 params.data.value 中。params.data 还包含 _metadata(Commerce 实例元数据)和 source(Commerce 事件配置中的商家/环境 ID 对)——二者都不是处理程序的输入。
  • Webhook 处理程序(通过 commerce-app-webhooks 接线):没有 data 包装层——Commerce 操作负载直接位于 params 中(例如 params.order),且响应必须使用 @adobe/aio-commerce-lib-webhooks/responses 中的操作辅助函数(参见 commerce-app-webhooks),而不是上文使用的普通 ok/buildErrorResponse 结构。
// Event handler — src/commerce-extensibility-1/actions/store-order/index.ts
export async function main(params: Record<string, unknown>) {
  const data = params.data as Record<string, unknown>;
  const value = data.value as Record<string, unknown>;
  let client;
  try {
    const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
    const token = await authProvider.getAccessToken();
    const db = await initDb({ token, region: "emea" });
    client = await db.connect();
    await client
      .collection("orders")
      .insertOne({ orderId: value.order_id, receivedAt: new Date() });
    return ok({ body: { processed: true } });
  } finally {
    if (client) await client.close();
  }
}
// Webhook handler — src/commerce-extensibility-1/actions/log-order/index.ts
import {
  ok,
  successOperation,
} from "@adobe/aio-commerce-lib-webhooks/responses";

export async function main(params: Record<string, unknown>) {
  const order = params.order as Record<string, unknown>; // Commerce operation payload, directly on params
  let client;
  try {
    const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
    const token = await authProvider.getAccessToken();
    const db = await initDb({ token, region: "emea" });
    client = await db.connect();
    await client
      .collection("orders")
      .insertOne({ orderId: order.entity_id, receivedAt: new Date() });
    return ok(successOperation());
  } finally {
    if (client) await client.close();
  }
}

请参阅 assets/db-action.ts,了解涵盖所有 CRUD 操作和游标迭代的完整注释示例。

Step 6 — 安装时设置集合和索引

对于 App Management 应用,请通过自定义安装步骤创建集合和索引——这是一段从 Commerce Admin 安装应用时运行一次、卸载时可以撤销的脚本。优先使用这种方式,而不是在首次请求时临时创建。

使用 defineCustomInstallationStep 编写该步骤(包含 install 处理程序和可选的 uninstall)。在其中,从 context.params(而非 config)解析 IMS 身份验证参数,然后遵循与 Step 5 相同的 init → connect → close 生命周期,并在集合对象上调用 createIndex:

// ./scripts/setup-database.ts — referenced from config as ./scripts/setup-database.ts
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
import {
  getImsAuthProvider,
  resolveImsAuthParams,
} from "@adobe/aio-commerce-lib-auth";
import { init as initDb } from "@adobe/aio-lib-db";

export default defineCustomInstallationStep({
  install: async (config, context) => {
    let client;
    try {
      // context.params carries the injected IMS credentials — NOT config.
      const authProvider = getImsAuthProvider(
        resolveImsAuthParams(context.params),
      );
      const token = await authProvider.getAccessToken();
      const db = await initDb({ token, region: "emea" }); // must match the manifest database.region
      client = await db.connect();

      const orders = client.collection("held_orders"); // get the collection object first
      await orders.createIndex({ order_id: 1 }, { unique: true }); // createIndex on the collection, not a name string
      return { status: "success" };
    } finally {
      if (client) await client.close(); // always close — avoids connection leaks
    }
  },
  uninstall: async (config, context) => {
    // Tear down your database state here.
    // Leave empty to preserve data across reinstalls.
  },
});

请使用 export default 将安装脚本编写为 ES 模块——切勿使用 module.exports。 安装操作会通过 import * as step from "<script>" 加载每个步骤,并读取 step.default,因此脚本必须默认导出 defineCustomInstallationStep(...) 的结果。使用 CommonJS 会导致问题:module.exports.default 会显示为 step.default.default,从而导致验证失败。script 路径必须以 .js 或 .ts 结尾——可以直接使用 TypeScript 编写,无需单独的编译步骤。

在 app.commerce.config.ts 的 installation.customInstallationSteps 下注册该步骤。script 路径直接指向你的 .ts 文件:

// app.commerce.config.ts
installation: {
  customInstallationSteps: [
    {
      script: "./scripts/setup-database.ts",
      name: "Set up held-orders collection",
      description: "Creates the held_orders collection and a unique index on order_id",
    },
  ],
},
字段限制
script相对于项目根目录的路径;必须是以 .js 或 .ts 结尾的 ES 模块(export default)
name非空字符串,最多 255 个字符;所有安装步骤中的名称必须唯一
description非空字符串,最多 255 个字符

完整的安装/卸载参考(含注释)请参阅 assets/setup-database.ts。

Step 7 — 重新生成 installation 操作

如果此自定义安装步骤是配置中的第一个需要安装的域(其他域为 webhooks、eventing.commerce、eventing.external、adminUi),请重新运行:

npx @adobe/aio-commerce-lib-app init

此操作会将 installation 操作添加到 ext.config.yaml 的 app-management 包中——aio app build/aio app deploy 只会读取现有文件,不会重新生成该文件。若跳过此步骤,即使脚本已在代码中注册且 DB 操作已部署,脚本也不会运行。即使该操作已存在,也可以安全地重新运行。

Step 8 — 验证

aio app build

构建失败时会直接指出有问题的配置字段。要使用真实数据库测试该操作,请进行部署并调用它(aio app deploy)。如果此步骤需要执行 Step 7,还要检查 ext.config.yaml 中的 app-management 下是否现在包含 actions.installation。

最佳实践

  • 始终在 finally 块中关闭连接——泄漏的连接会耗尽资源。
  • 匹配区域——清单中的 database.region 是唯一可信来源;每次 init() 调用和安装步骤都必须使用该值(参见步骤 1 中的区域提示)。区域不匹配时,连接会静默失败,调用方无法得知。
  • 对频繁查询的字段使用投影(.project({ field: 1 }))和索引(createIndex);索引字段总长度必须 ≤ 2048 字节。
  • 使用游标遍历大型结果集(for await (const doc of collection.find(...))),而不是调用 toArray(),以限制内存占用。
  • 不要硬编码区域或密钥——优先使用 AIO_DB_REGION 和注入的 IMS 令牌,而不是内联值。
  • 在运行时操作中,优先使用最具体的 Adobe I/O 库,而不是统包的 @adobe/aio-sdk——例如,使用 @adobe/aio-commerce-lib-auth 进行 IMS 身份验证,使用 @adobe/aio-lib-core-logging 作为日志记录器,以缩小操作包的体积。
  • 使用自定义安装步骤(defineCustomInstallationStep,参见步骤 6)在安装期间设置集合和索引,而不是等到首次请求时再临时设置——该步骤会在通过 Commerce Admin 安装应用时运行一次,并可在卸载时撤销。只有当应用不是通过 App Management 安装时,通用的 App Builder post-app-deploy 钩子才是替代方案。

常见问题

  • 令牌有效但连接失败:操作缺少 include-ims-credentials: true,或者尚未在 Adobe Developer Console 中将 App Builder Data Services API 添加到项目(参见先决条件)。

  • 运行 aio app deploy 后未预配数据库(仅扩展应用):aio app CLI 插件中的一个 bug(与 aio-lib-db 无关)会在 application 运行时清单没有运行时操作时跳过声明式自动预配。应用第 1 步中的仅扩展应用解决方法——在 application 下添加 packages: {} 和 post-app-build: "mkdir -p dist/application/actions" 钩子——或在本地开发时使用 CLI 备用方案预配一次。

  • 更改区域后连接失败:库的区域与清单中的 database.region 不匹配。切换区域具有破坏性——运行 aio app db delete,更新清单中的 database.region,然后重新预配(运行 aio app deploy,或在本地开发时使用 CLI 备用方案)。

  • 按字符串形式的 _id 查询无结果:先将其转换——使用 bson 中的 new ObjectId(idString)。原始字符串永远无法匹配存储的 ObjectId。

  • DbError 与意外错误:服务抛出的错误具有 name === "DbError";根据此属性分支,以区分数据库故障与应用 bug。

  • findOne 在未匹配时抛出错误,而不是返回 null:与 MongoDB 不同,未匹配并不会成功返回 null,而是会抛出 DbError,其消息中包含 "Document not found"。仅检查 name === "DbError" 不足以检测这种情况,因为每个 DbError(包括真正的连接故障)都有此名称;还要检查 error.message:

    function isDocumentNotFoundError(error: unknown) {
      const e = error as { name?: string; message?: string };
      return e.name === "DbError" && e.message?.includes("Document not found");
    }
    
    let existing;
    try {
      existing = await records.findOne({ order_id: orderId });
    } catch (error) {
      if (!isDocumentNotFoundError(error)) {
        throw error;
      }
    }
    

    完整参考请见 assets/db-action.ts。

  • 安装步骤中的身份验证失败:从 context.params 解析 IMS 身份验证参数(resolveImsAuthParams(context.params))——其中包含注入的 AIO_COMMERCE_AUTH_IMS_* 凭据——而不是从不包含凭据的 config 中解析。请使用 @adobe/aio-commerce-lib-auth,而不是 @adobe/aio-lib-core-auth:后者的 generateAccessToken 需要直接提供 clientId/clientSecret,无法使用注入的参数。

  • 安装步骤加载失败(must export a default function or object):脚本是用 CommonJS 编写的。请将其编写为带有 export default 的 ES 模块;通过框架的 import * as 加载器导入时,module.exports(或 module.exports.default)会显示为 .default.default,导致验证失败。

  • createIndex 报错或不起作用:必须在集合对象上调用(client.collection("name").createIndex({ field: 1 })),而不是传入集合名称字符串。先获取集合,再在其上调用 createIndex。

  • 已注册并部署自定义安装步骤,但从未运行:注册第一个需要安装的域后(第 7 步)没有重新运行 init——没有 installation 操作,也就没有安装端点。

质量标准

  • aio app build 无错误完成
  • 每个由用户编写的 DB 操作都在其注释中声明 include-ims-credentials: true
  • 操作在 finally 块中关闭客户端,并在清单 database 块声明的区域初始化库
  • 当此自定义安装步骤需要 installation 操作时,该操作必须存在于 ext.config.yaml 中

链接

  • 将操作关联到事件——调用 commerce-app-eventing,并在事件的 runtimeActions 中引用此操作。
  • 将操作关联到 Webhook——调用 commerce-app-webhooks,并通过 runtimeAction 引用此操作。
  • 从 Admin UI 触发操作——调用 commerce-app-admin-ui,添加批量操作、订单视图按钮或网格列,以调用此运行时操作。

参考资料

  • assets/db-action.ts — 完整的注释处理程序:初始化/连接、CRUD、游标迭代和关闭生命周期
  • assets/setup-database.ts — 完整的自定义安装步骤注释:安装时创建集合和唯一索引,卸载时删除集合,并介绍 context.params 和在集合上调用 createIndex 的模式

评论

支持 Markdown