From a9e3d59724326bac1226c55724603df438a9750d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:01:38 +0000 Subject: [PATCH] =?UTF-8?q?docs(kernel):=20services.data=20=E9=A1=B5?= =?UTF-8?q?=E7=9A=84=20Example=20=E5=8E=BB=E6=8E=89=20hook=20=E8=AF=AD?= =?UTF-8?q?=E5=A2=83=E6=B7=B7=E6=90=AD,=E5=B9=B6=E8=AF=B4=E6=B8=85=20Canon?= =?UTF-8?q?ical=20source=20=E4=B8=BA=E4=BD=95=E6=98=AF=20SDK=20(#5944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原 Example 把「hook 才有的 `ctx.input.contact_id`」和「hook 拿不到的 `services.data`」拼在一起。实测该块不是「在 hook 里跑得慢」而是根本不成立: 按 check:skill-examples 同款 tsconfig 编译,`services` 与 `ctx` 双双 TS2304 (Cannot find name),因为这两个名字在本运行时里没有任何一处同时在作用域内 —— hook 有 `ctx` 没 `services`,持有绑定的代码有 `services` 没 `ctx`。 复核 #5720 的结论在 origin/main 未变:engine.ts 四处 `hookContext` 构造点 (4796/4929/5505/6059)逐键构造 object/event/input/session/provenance/user/ api/transaction/ql,`buildSandboxContext`(runtime/src/sandbox/body-runner.ts) 同样只产 input/previous/user/session/event/object/result/api/log/crypto —— 两条路径都没有 `services` 键。 照 PR #5938 定下的模式改(取「去掉 `ctx.` 的 hook 味道」那一支): - Example 改写为**持有该绑定的代码**在调用(形状对齐 sharing-service.mdx 的 `mayEditContract(sharing: ISharingService, …)`):记录 id 作为普通入参传入, 全块无 `ctx`。顺带修掉两处误读 —— `get` 返回的是信封 `{ object, id, record }` 而非行本身(原文 `contact.id` 是撞对的),`find` 返回 `{ records, … }`。 - 新增 binding note Callout:hook 没有 `services` 键,其跨对象通道是 `ctx.api`, 并指向 examples.mdx 第 2 节(#5938 已进 os:check 真编译的那块),不在本页重复。 - 新增 `## Canonical source` 一节,答分诊点名的措辞问题:本页是全章唯一不指向 `contracts/*-service.ts` 的页,因为 spec 根本没有 `IDataService`;最近邻 `IDataEngine` 是形状不同的更低一层面(`find(objectName, query, options) : Promise`)。故签名取 `ObjectStackClient.data`,返回体对应 spec 的 `*DataResponseSchema`(protocol.zod.ts),托管运行时按同一形状绑定。 os:check 标记:不补,且与 #5945 无关 —— 反向验证给出的是另一条硬约束。给该块 加标记后实测: content/docs/kernel/runtime-services/data-service.mdx:88:40 error TS2307: Cannot find module '@objectstack/client' or its corresponding type declarations. 仅此一条诊断。check:skill-examples 的 paths 由 `@objectstack/spec` 自己的 exports 派生,而 spec 不依赖 client,所以标记后只能改成手写 `DataService` 局部类型 —— 那样这块就只钉住它自己。此约束已写进正文,待本面有 spec 侧契约 可 import 时再补标记。本块从不触碰 `ctx.api`,故不受 #5945 裁决影响。 门禁:check:doc-authoring 362 文件干净;check:nul-bytes 5745 文件干净; check:docs-audit-scope 绿;check:skill-examples 207 块全绿(未变,本页不贡献 标记块也不产生 orphan)。Example 块单独对已构建的 @objectstack/client 声明 编译:tsc --noEmit 退出 0。MDX 以 @mdx-js/mdx 编译通过(examples.mdx / sharing-service.mdx 作阳性对照)。 Fixes #5944 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3 --- .../kernel/runtime-services/data-service.mdx | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/content/docs/kernel/runtime-services/data-service.mdx b/content/docs/kernel/runtime-services/data-service.mdx index fe212b04e7..89789791bf 100644 --- a/content/docs/kernel/runtime-services/data-service.mdx +++ b/content/docs/kernel/runtime-services/data-service.mdx @@ -6,7 +6,28 @@ description: CRUD runtime helper API for records (`get`, `find`, `create`, `upda # `services.data` - **Stability:** `stable` -- **Canonical source:** `packages/client/src/index.ts` +- **Canonical source:** `packages/client/src/index.ts` — the `ObjectStackClient.data` + surface (see [Canonical source](#canonical-source) for why this page names the SDK + rather than a `contracts/*-service.ts` interface) + + + +This page documents the `services.data` **contract surface**: the signatures, not a +binding every runtime surface receives (see the +[binding note](/docs/kernel/runtime-services)). A **data hook never gets one.** The engine +builds a hook context key by key — `object` / `event` / `input` / `session` / `provenance` +/ `user` / `api` / `transaction` / `ql` — and sets no `services` key at any of its +construction sites, so `services.data.get(…)` written beside a `ctx.input.…` read throws +on `services` at the first call rather than reading anything +([#5720](https://github.com/objectstack-ai/objectstack/issues/5720)). + +A hook's own cross-object channel is `ctx.api`: see +[Examples §2](/docs/kernel/runtime-services/examples) for a +`ctx.api.object('crm_account').findOne(…)` read inside a real `beforeInsert` / +`beforeUpdate` handler. The [Example](#example) below is written the other way round — as +the code that **holds** the binding calls it, with plain arguments and no `ctx` in sight. + + ## Methods @@ -18,6 +39,22 @@ services.data.update(object: string, id: string, data: Partial): Pro services.data.delete(object: string, id: string): Promise ``` +## Canonical source + +Every sibling page in this chapter names a contract interface +(`packages/spec/src/contracts/sharing-service.ts`, `queue-service.ts`, …). This one names +the **client SDK** instead, and the difference is real rather than an oversight: the spec +declares no `IDataService`. Its nearest neighbour, `IDataEngine` +(`packages/spec/src/contracts/data-engine.ts`), is a *lower* surface with a different +shape — `find(objectName, query, options): Promise` straight at the engine — not +the object-name-plus-options protocol call documented above. + +So the signatures come from `ObjectStackClient.data` (`packages/client/src/index.ts`), and +the payloads they resolve to are the spec's wire schemas — `GetDataResponseSchema`, +`CreateDataResponseSchema`, `UpdateDataResponseSchema`, `DeleteDataResponseSchema` in +`packages/spec/src/api/protocol.zod.ts` — which the SDK's `*DataResult` interfaces mirror +key for key. A managed runtime binds `services.data` to this same shape. + ## Parameters - `object`: short object name (for example `task`, `account`) @@ -41,11 +78,37 @@ services.data.delete(object: string, id: string): Promise ## Example +Call these methods from code that **holds** the binding — a managed runtime hands it in as +`services.data` — so the record id arrives as an ordinary argument. It is deliberately not +a hook body: a hook has no `services` key to reach through (see above), and reads other +objects via `ctx.api`. + ```ts -const contact = await services.data.get('contact', ctx.input.contact_id); -const orders = await services.data.find('sales_order', { - filter: { contact_id: contact.id }, - sort: [{ field: 'created_at', order: 'desc' }], - top: 20, -}); +import type { ObjectStackClient } from '@objectstack/client'; + +/** The `services.data` binding, exactly as this page's Canonical source declares it. */ +type DataService = ObjectStackClient['data']; + +export async function recentOrdersForContact(data: DataService, contactId: string) { + // `get` resolves the response envelope `{ object, id, record }` — the row is `record`. + const { record: contact } = await data.get<{ id: string; name: string }>('contact', contactId); + + // `find` resolves `{ records, total?, hasMore? }`. + const { records: orders } = await data.find<{ id: string; amount: number }>('sales_order', { + filter: { contact_id: contact.id }, + sort: [{ field: 'created_at', order: 'desc' }], + top: 20, + }); + + return { contact, orders }; +} ``` + +The block carries no `{/* os:check */}` marker, and that is a measurement rather than an +omission: `check:skill-examples` compiles marked blocks against the built +`@objectstack/spec` declarations only — its `paths` map is derived from that package's own +`exports`, and `@objectstack/spec` does not depend on `@objectstack/client`. A marked +block here would therefore have to hand-declare `DataService` instead of importing it, +which pins the example to itself and nothing else. The marker becomes worth adding the day +this surface has a spec-side contract to import (see the +[Canonical source](#canonical-source) note).