关于 cap-add-remote-service
cap-add-remote-service 按照 Calesi 模式,引导开发者为现有 CAP Node.js 项目添加远程服务集成。内容涵盖 API 导入、消费视图、领域模型修改、处理程序和验证,并说明数据复制与委托读取的选择。
主要功能
- 01检查 CAP 项目结构和服务
- 02从 npm 包或 EDMX 导入 API
- 03定义消费视图和领域映射
- 04将远程实体接入领域模型
- 05实现委托、数据复制或 Outbox 事件
- 06使用 CAP 工具验证集成
适用场景
- 01为 CAP 项目添加 OData API
- 02通过应用服务公开远程实体
- 03选择与本地记录联接的数据策略
- 04在本地开发中测试服务集成
MCP 相关
该技能使用 `cds-mcp_search_model` 检查项目中已有的服务和实体。输入未说明 MCP 服务或向 MCP 客户端公开的工具。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ capire/ skills --skill cap-add-remote-service安装后在 Claude Code 中输入/cap-add-remote-service确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonevals/fixture/package.jsonevals/fixture/apis/xflights.cdsevals/fixture/data/API_BUSINESS_PARTNER.edmxevals/fixture/db/schema.cdsevals/fixture/srv/travel-service.cdstravel-service.js详细介绍
我会做什么
按照 CAP-Level Service Integration(Calesi)模式,逐步引导开发者为现有 CAP Node.js 项目添加远程服务集成。这包括导入 API、定义消费视图、集成模型,以及接入所需的 Node.js 处理程序。
我需要先了解的上下文
开始之前,先检查项目,了解其结构:
- 阅读
package.json,了解项目名称和现有依赖项。 - 使用
cds-mcp_search_model,概览项目中已定义的服务和实体。 - 询问用户:
- 想集成哪个远程服务(另一个 CAP 应用、S/4HANA Business Partner 等)
- 他们是否已有 npm 包形式的 API、
.edmx文件,还是只有 URL
步骤 1 — 导入 API
选项 A:来自 npm 的 CAP 包
如果远程服务以 CAP 包的形式发布(例如 @capire/xflights-data):
npm add <package-name>
检查新增包的 index.cds 和 package.json,了解它公开的服务名称和实体。
重要:还要检查该包的模拟数据 CSV 文件(通常位于 srv/external/data/),确认哪些列实际包含数据。消费视图只应投影模拟数据中存在的列,否则本地开发时 UI 会显示空值。(如不确定,请向用户确认)
选项 B:OData EDMX 文件
如果开发者提供了 .edmx 文件(例如从 SAP Business Accelerator Hub 下载的文件):
cds import path/to/API_NAME.edmx
此操作会将 .edmx 复制到 srv/external/,并生成 .csn 文件。如果需要可读性更好的 .cds 文件,请使用 --as cds。
导入后,检查 srv/external/ 中生成的内容:
ls srv/external/
如果 package.json 的 cds.requires 中尚未包含该服务,请添加:
"cds": {
"requires": {
"API_NAME": {
"kind": "odata-v2"
}
}
}
对于 OData v4 服务,请使用 "kind": "odata"。
步骤 2 — 创建消费视图
创建一个新的 .cds 文件(例如 apis/<source-system>.cds 或
srv/external/<name>.cds),其中包含一个消费视图——将导入的定义映射到领域模型,并按需重命名和扁平化。
// Example: mapping S/4 A_BusinessPartner to a domain-friendly Customers entity
using { API_BUSINESS_PARTNER as S4 } from '@capire/s4';
namespace sap.capire.s4;
@federated entity Customers as projection on S4.A_BusinessPartner {
BusinessPartner as ID,
PersonFullName as Name,
LastChangeDate as modifiedAt,
} where BusinessPartnerCategory == 1;
// Example: mapping xflights data to a domain-friendly Flights entity
using { sap.capire.flights.data as x } from '@capire/xflights-data';
namespace sap.capire.xflights;
@federated entity Flights as projection on x.Flights {
ID, date, departure, arrival, modifiedAt,
airline.icon as icon,
airline.name as airline,
origin.name as origin,
destination.name as destination,
}
主要约定:
- 对需要本地数据复制的实体使用
@federated(参见步骤 5)。 - 将远程名称映射为领域名称(例如,
BusinessPartner→ID)。 - 在适用时将关联扁平化为列(适用于 HCQL,不适用于普通 OData)。
- 如有需要,添加
where子句以预先筛选数据。 - 如果该包为源实体添加了
@cds.minify:'unused-elements'注解,必须在消费视图文件中覆盖它:
否则,投影将没有任何元素,部署会因“Expecting view to have at least one non-virtual element”而失败。annotate S4.A_BusinessPartner with @cds.minify:false;
步骤 3 — 在领域模型中引用远程实体
在 db/schema.cds(或相关领域模型文件)中,导入消费视图并添加关联:
using { sap.capire.xflights as x } from '../apis/capire/xflights';
entity Bookings {
// ... existing elements ...
Flight : Association to x.Flights;
}
如果远程实体要替代本地实体(例如,用 S/4 Business Partner 替代本地 Customers),请彻底移除本地实体定义,并改为导入消费视图。然后更新现有测试数据 CSV,使其使用与远程模拟数据匹配的 ID(例如,使用 000001 而不是 1004155)。
同时更新任何引用新远程实体上已不存在字段的 UI 注解(值列表、文本排列)。
如果还需要从远程实体反向关联到本地实体,请使用非托管关联(远程端没有外键):
extend x.Flights with columns {
Bookings : Association to many Bookings on Bookings.Flight = $self;
}
非托管关联和虚拟/计算字段是扩展远程实体时唯一安全的方式——避免使用常规元素或托管关联。
步骤 4 — 在服务中公开远程实体(供 UI 使用)
如果 Fiori UI 或 OData 服务需要访问远程实体,请在应用服务中将其公开为只读投影:
// In srv/my-service.cds
using { sap.capire.xflights as x } from '../apis/capire/xflights';
service MyService {
// ... existing entities ...
@readonly entity Flights as projection on x.Flights;
}
否则,与未公开实体的关联会被截断。
步骤 5 — 配置集成处理程序(Node.js)
打开(或创建)服务处理程序文件(例如 srv/my-service.js)。
5a — 连接到远程服务
传递给 cds.connect.to() 的服务名称必须与远程包的 index.cds 中定义的服务名称完全一致(对于 EDMX 导入,则必须与 cds.requires 中使用的键一致)。请务必查找确认,不要自行编造别名。
module.exports = class MyService extends cds.ApplicationService {
async init() {
const remote = await cds.connect.to('sap.capire.flights.data')
// or: cds.connect.to('API_BUSINESS_PARTNER')
const { Flights, Customers } = this.entities
// ... handlers below ...
return super.init()
}
}
5b — 委托(用于值帮助 / 按需读取)
将传入的远程实体 READ 请求直接委托给上游服务。CAP 会自动通过消费视图转换查询:
this.on('READ', Customers, req => s4.run(req.query))
5c — 数据联邦(用于与本地数据联接)
当本地数据需要与远程数据一起使用时(例如,在显示预订的同时显示航班详情),有两种方法。请询问用户偏好哪种方法,并说明各自的利弊:
选项 1:在本地复制数据
将远程数据复制到本地持久化表,以便原生执行 SQL JOIN。
优点:
- 完整支持 SQL JOIN(可跨本地和远程数据筛选、排序和分页)
- 读取速度快 — 查询时无需网络往返
- 可离线运行 / 远程服务不可用时仍可使用
缺点:
- 数据可能过时(新鲜度取决于上次复制的时间)
- 需要复制逻辑和调度(初始加载 + 增量同步)
- 占用本地存储空间
- 需要维护和监控更多环节
实现:
为消费视图添加注解,以持久化复制的数据:
// In db/schema.cds
annotate x.Flights with @cds.persistence.table;
在服务处理程序中添加复制逻辑(例如,在启动时或定时执行):
const { Flights } = cds.entities('sap.capire.xflights')
let { latest } = await SELECT.one`max(modifiedAt) as latest`.from(Flights)
let touched = await xflights.read(Flights).where`modifiedAt > ${latest || 0}`
if (touched.length) await UPSERT(touched).into(Flights)
或者将 @federated(已添加到消费视图)与
CAP Data Federation
插件搭配使用,以实现自动复制。
选项 2:将读取委托给远程服务(不复制)
在请求时将远程实体的 READ 请求转发给上游服务,从而始终获取最新数据。
优点:
- 始终保持最新 — 无过时数据
- 无需本地存储或复制逻辑
- 对于只读场景,配置更简单
缺点:
- 不支持跨本地和远程数据的原生 SQL JOIN
- 每次读取都会产生网络往返(延迟、可用性风险)
- 如无额外的应用层逻辑,不支持跨本地和远程字段筛选 / 排序
- 如果远程服务宕机,读取会失败
实现:
在服务处理程序中委托 READ 请求:
this.on('READ', Flights, req => xflights.run(req.query))
无需添加 @cds.persistence.table 注解。从本地 DB 的角度来看,该实体仍是虚拟的。
建议
如果需要将远程数据与本地数据联接、筛选或排序,或者需要应对远程服务中断,请使用复制。如果数据新鲜度至关重要、数据集规模庞大 / 变化频繁,或者实体仅单独访问(例如值帮助、详情视图),请使用委托。
重要提示: 如果项目架构中有一个本地实体通过 Association to 关联到远程实体,并且该本地实体在列表视图中公开(即需要执行 SQL
JOIN),则必须进行复制 — 委托无法支持跨服务边界的 SQL JOIN。该关联在运行时会静默返回 null。在此情况下无需询问用户 — 请直接选择复制。
5d — Outboxed 事件(可靠的即发即弃写入)
对于不需要同步返回结果的远程服务写入操作:
const xflights_ = cds.outboxed(xflights)
this.after('SAVE', Travels, ({ Bookings = [] }) => {
return Promise.all(Bookings.map(b => {
let { Flight_ID: flight, Flight_date: date } = b
return xflights_.send('POST', 'BookingCreated', { flight, date })
}))
})
此操作会在同一个 DB 事务中将事件存储到本地 outbox 表中,然后异步转发并在失败时重试。
第 6 步 — 安装 Cloud SDK 前置依赖
远程 HTTP 通信需要 SAP Cloud SDK 软件包。检查是否已安装;如果没有,请添加:
npm add @sap-cloud-sdk/connectivity @sap-cloud-sdk/http-client @sap-cloud-sdk/resilience
第 7 步 — 使用 cds watch 验证
启动应用并检查日志输出:
cds watch
仔细检查连接行。 你应该看到以下其中一种:
mocking <service-name>— CAP 正在进程内自动模拟(适合开发)connect to <service-name> > odata { url: ... }— CAP 正在连接外部模拟进程
如果你看到 connect to ... > odata 指向某个 URL,但那里没有运行模拟进程,READ 请求将静默失败并返回空结果。这种情况会在 ~/.cds-services.json 中存在上一次 cds mock 运行留下的过期条目时发生。可通过以下任一方式修复:
- 启动模拟进程:
cds mock <service-name> - 或删除过期条目,让 CAP 回退到进程内模拟
预期结果:类似以下日志行:
[cds] - mocking sap.capire.flights.data { at: [...], ... }
这表明 CAP 正在为本地开发自动模拟远程服务——在内部循环开发期间无需上游服务。
要测试真实委派(独立模拟进程):
# Terminal 1
cds mock apis/capire/xflights.cds
# Terminal 2
cds watch
检查使用该服务的服务现在记录的是 connect to ... > hcql(或 odata),而不是 mocking。
第 8 步 — 在 cds repl 中测试查询
无需启动 UI,即可快速验证集成:
cds repl ./
// Connect to the service
const svc = await cds.connect.to('sap.capire.flights.data')
const { Flights } = cds.entities('sap.capire.xflights')
// Read through the consumption view
await svc.read(Flights).limit(3)
// Check that column mapping worked (should see domain names, not remote names)
关键模式摘要
| 场景 | 模式 |
|---|---|
| 按需读取远程数据(例如值帮助) | 委派:this.on('READ', Entity, req => remote.run(req.query)) |
| 本地 SQL JOIN 需要远程数据 | 数据联合:@federated + @cds.persistence.table + 复制逻辑 |
| 向远程服务发送即发即忘写入 | Outboxed 发送:cds.outboxed(remote).send(...) |
| 上游服务未运行时进行本地开发 | 自动处理 — CAP 在进程内模拟;无需配置 |
| 重命名或重塑远程 API 以适配领域模型 | 消费视图(带列别名和 where 子句的投影) |
应避免的常见错误
- 不要在服务中直接暴露原始导入实体——始终通过消费视图或投影访问。
- 不要在消费视图中使用
view关键字——始终使用entity ... as projection on。(view是不同的 CDS 概念。) - 不要在消费视图上使用
@cds.persistence.skip——这是不同的注解。改用@federated(将实体标记为用于数据联合),并在需要本地复制时单独添加@cds.persistence.table。 - 不要在处理程序注册中使用字符串实体名称——始终从
this.entities解构:const { Flights } = this.entities,然后使用this.on('READ', Flights, ...),而不是this.on('READ', 'Flights', ...)。 - 不要在
cds.connect.to()中自行编造服务名称——对于 EDMX 导入,请在软件包的index.cds或cds.requires中查找。 - 不要在克隆 CQN 对象前修改它们:
const q2 = cds.ql.clone(q1)。 - 不要向远程实体扩展添加常规元素或受管关联——只能添加非受管关联、虚拟字段和计算字段。
- 不要在未启用数据联合时,依赖实时跨服务导航来筛选或排序——这会在运行时失败。
- 不要忘记 Cloud SDK 软件包——缺少这些软件包时,远程 HTTP 调用会因缺少依赖项而报错。
- 不要投影远程模拟数据中不存在的列——UI 会显示空白值。务必将模拟 CSV 表头与消费视图列交叉核对。
- 不要在用远程实体替换本地实体时忘记更新测试数据外键——旧 ID 与远程模拟数据不匹配。
- 不要忽略导入实体上的
@cds.minify注解——它们可能会从投影中移除所有元素,导致部署失败。