mcpField
C
cap-add-remote-service

cap-add-remote-service

github.com/capire开发17161 次安装4 天前更新
代码仓库

关于 cap-add-remote-service

cap-add-remote-service 按照 Calesi 模式,引导开发者为现有 CAP Node.js 项目添加远程服务集成。内容涵盖 API 导入、消费视图、领域模型修改、处理程序和验证,并说明数据复制与委托读取的选择。

主要功能

  1. 01检查 CAP 项目结构和服务
  2. 02从 npm 包或 EDMX 导入 API
  3. 03定义消费视图和领域映射
  4. 04将远程实体接入领域模型
  5. 05实现委托、数据复制或 Outbox 事件
  6. 06使用 CAP 工具验证集成

适用场景

  1. 01为 CAP 项目添加 OData API
  2. 02通过应用服务公开远程实体
  3. 03选择与本地记录联接的数据策略
  4. 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.json
evals/fixture/
package.json
evals/fixture/apis/
xflights.cds
evals/fixture/data/
API_BUSINESS_PARTNER.edmx
evals/fixture/db/
schema.cds
evals/fixture/srv/
travel-service.cds
travel-service.js

详细介绍

我会做什么

按照 CAP-Level Service Integration(Calesi)模式,逐步引导开发者为现有 CAP Node.js 项目添加远程服务集成。这包括导入 API、定义消费视图、集成模型,以及接入所需的 Node.js 处理程序。

我需要先了解的上下文

开始之前,先检查项目,了解其结构:

  1. 阅读 package.json,了解项目名称和现有依赖项。
  2. 使用 cds-mcp_search_model,概览项目中已定义的服务和实体。
  3. 询问用户:
    • 想集成哪个远程服务(另一个 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' 注解,必须在消费视图文件中覆盖它:
    annotate S4.A_BusinessPartner with @cds.minify:false;
    
    否则,投影将没有任何元素,部署会因“Expecting view to have at least one non-virtual element”而失败。

步骤 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 注解——它们可能会从投影中移除所有元素,导致部署失败。

评论

支持 Markdown