关于 commerce-app-storage
此技能用于将 App Builder Database Storage 集成到现有 Adobe Commerce 应用,面向开发者提供数据库配置、运行时操作和安装步骤的指导。
主要功能
- 01配置工作区数据库
- 02Web 与事件操作模式
- 03文档增删查改
- 04安装时创建集合和索引
- 05认证与区域配置指导
适用场景
- 01将 Commerce 应用数据存入数据库文档
- 02通过数据库操作处理 HTTP 请求
- 03处理 Commerce 事件或 Webhook
- 04在应用安装时准备集合和索引
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ adobe/ skills --skill commerce-app-storage安装后在 Claude Code 中输入/commerce-app-storage确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/db-action.tssetup-database.tsevals/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 Builderpost-app-deploy钩子才是替代方案。
常见问题
-
令牌有效但连接失败:操作缺少
include-ims-credentials: true,或者尚未在 Adobe Developer Console 中将 App Builder Data Services API 添加到项目(参见先决条件)。 -
运行
aio app deploy后未预配数据库(仅扩展应用):aio appCLI 插件中的一个 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的模式