Repository navigation
Commit 63b86b3
fix(rest): the approvals, email, sharing, sharing-rule and analytics routes answer from the request's environment kernel when one REST plugin fronts many environments (#30013)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} -->
_Requested by **Sam** · [project
thread](https://claude.ai/code/project/chan_01GsBogozeZG5sdpAARrN7HP?thread=cmsg_01GsBogozeZG5sdpAARrN7HPSQS59N7Px4367s49jMWcuS)_
Part of #30011
This PR is the whole of that card. It says `Part of` rather than a
closing keyword because the card carries no claim naming a
`claude/issue-*` branch and this PR's head branch cannot take that
shape, so `check:closing-target-claim` could never pass with a closing
keyword. The seat closes the card when this lands.
Refs #29999 (the defect record the card implements; it stays open for
cloud's staging re-read).
Clause-②: no
Metadata-protocol: none
_Keep one value on each of the two lines above, each line starting with
its key.
[Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum)
item 5 says what they mean._
## 维护者速读
- 改了什么:一个 REST
插件挂在宿主内核前面、后面有多个环境内核时(云上托管运行时),审批、邮件、共享、共享规则、分析这五类接口改为到请求所属环境的内核里取服务,不再到宿主内核里取。
- 为什么改:宿主内核上没有这些服务,所以每个托管环境的审批中心列表都是空的、单条审批打开是
501,记录页的审批区也是空的,共享、共享规则和邮件接口一律 501(#29999)。
- 风险与回滚:只影响注册了 kernel-manager
的部署;单内核(社区版自部署)走原路,有测试钉住。分析接口在环境缺分析服务时不再借用宿主的分析服务,改为返回 501。回滚即 revert 本
PR,无配置或数据迁移。
- 先看:`packages/rest/src/rest-api-plugin.ts`(`environmentScopedProvider`
一段,风险在“不回退到宿主”那条腿)、`packages/rest/src/analytics-routes.ts`(删掉宿主回退的
`resolveService`)、`packages/rest/src/rest-environment-scoped-providers.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/rest exec vitest run
src/rest-environment-scoped-providers.test.ts`,预期 1 个文件、58 个测试全部通过。
- 需要你做的:无,席位落地。
## What changes
`packages/rest/src/rest-api-plugin.ts` builds the five provider seams it
hands `RestServer` (`email`, `sharing`, `approvals`, `sharingRules`,
`analytics`) from one factory, `environmentScopedProvider(slot)`. Each
answers by these legs, in order:
- **An environment id and a registered `kernel-manager`**: read the slot
on that environment's kernel, acquired with
`kernelManager.getOrCreate(id)`, the same acquisition the data path's
`resolveProtocol` makes. "Never registered" (the branded registry error)
is `undefined`, and any other fault (a factory that threw) is re-raised,
matching `objectQLProvider`'s three-state classification. There is no
fallback to the plugin's own (host) occupant.
- **The reserved `platform` id, no id, or no `kernel-manager`**:
`ctx.getService(slot)` on the plugin's own kernel, exactly as before.
The provider resolves no environment itself. Each route asks the one
existing resolver,
`RestServer.resolveRequestEnvironmentId(environmentId, req)`, and hands
its answer to the provider:
- the scoped mount (`/environments/:environmentId/...`) answers the
param;
- the unscoped (hostname-routed) mount answers through the host's
`kernel-resolver` (`resolveEnvironment` first, so the id lookup buys no
kernel), then the hostname / `X-Environment-Id` chain, as the data
routes do.
Route-side, each moved route body's host interface gains
`resolveRequestEnvironmentId` (typed as the class member), and
`rest-server.ts` adds it to the host literals:
- `approvals-routes.ts`: `resolveService(environmentId, req)` at all 8
call sites.
- `sharing-routes.ts`: the same for the shares doors and the
sharing-rule doors (8 call sites).
- `email-routes.ts`: `POST /email/send`.
- `record-page-routes.ts`: the approvals part of `POST
/data/:object/:id/page`.
- `analytics-routes.ts`: `resolveService` keeps the shared resolver call
it already made, drops its own inline `kernelManager` lookup (now the
provider's) and drops the host fallback. `kernelManager` leaves
`AnalyticsRouteHost` and the analytics host literal.
A changeset (`@objectstack/rest` patch) carries the same declaration
lines.
## Why
On cloud's hosted runtime one `RestApiPlugin` sits on the host kernel in
front of many environment kernels, and each environment registers these
five services on its own kernel. The providers read `ctx.getService` on
the host kernel whatever id they were handed, so the environment's
services were never reached (#29999). The data routes never had this
problem because they resolve the request's kernel; this change makes the
five seams use that same resolution.
The reading per provider, as the card asked (step 2), measured by the
hosted-shape test below against the base sources:
| Provider | Doors | Hosted shape before this PR |
|:--|:--|:--|
| `approvals` | `GET /approvals/requests`, `GET /approvals/requests/:id`
(and the lock, decide, recall and action doors on the same seam), record
page approvals part | **Wrong.** Host-only read; the host carries no
`approvals`, so the list answered `{ data: [] }`, a single request 501
and the record page part `{ requests: [], lock: null }`. |
| `sharing` | `/data/:object/:id/shares` | **Wrong.** Host-only read;
the host mounts no sharing plugin, so 501. |
| `sharingRules` | `/sharing/rules/*` | **Wrong.** Same as `sharing`
(the sharing plugin registers both slots on the environment kernel), so
501. |
| `email` | `POST /email/send` | **Wrong.** Host-only read; the host
mounts no email plugin, so 501. |
| `analytics` | `POST /analytics/dataset/query` | **Right when the
environment carries analytics** (the route already read the
environment's kernel first), **wrong otherwise**: when the environment's
kernel carried no analytics service, or could not be acquired, the route
fell back to the host's provider instead of answering from the request's
environment alone. |
## Tests
- **New** `packages/rest/src/rest-environment-scoped-providers.test.ts`
(58). It boots real `ObjectKernel`s: a host kernel carrying the real
`createRestApiPlugin` (scoping on, `projectResolution: 'auto'`), a
`kernel-manager` double with the real manager's dedupe of concurrent
builds and build / acquisition counters, a `kernel-resolver` mapping the
`Host` header to an environment, and host-tagged occupants of all five
slots. Behind it are three environment kernels: `env_alpha` and
`env_beta` each carry auth, a protocol, an engine and their own tagged
occupants of the five slots, and `env_bare` carries none of the five.
Seven doors are driven through the real mounted handlers: approvals
list, approvals get, shares list, sharing rules list, email send, the
inline dataset query and the record page's approvals part.
- **Isolation (the card's hard criterion):** every door × {scoped,
hostname} × {A, B} answers 200 with the target environment's row, and
exactly one downstream call is recorded, the target's: none from the
host, none from the other environment (28 cases).
- **No host fallback:** every door × {scoped, hostname} for `env_bare`
gets the door's own absent answer (`{ data: [] }` for the list, `{
requests: [], lock: null }` for the record page part, 501 elsewhere) and
reaches no occupant at all (14 cases).
- **The card's approvals criterion:** environment A's pending request is
answered on both mounts for A, and is a 404 through environment B, with
B's service the one asked.
- **One resolution, one kernel:** a hostname-routed request for a cold
environment never calls `resolveKernel` to learn the id, and the kernel
manager builds exactly that one environment once.
- **Single kernel (the card's second hard criterion):** with no
`kernel-manager` and no `kernel-resolver`, every door on both mounts is
answered by the one kernel's occupant (14 cases).
- **Updated** `packages/rest/src/rest-api-plugin-slot-lookups.test.ts`
(8). Each provider row now says whether it is host- or
environment-scoped.
- The slot-identity pin checks an environment-scoped provider against
the environment kernel's occupant for `'env_1'`, and against the host's
for no id and for `'platform'`.
- The asked-slots pin records the five slots asked on the environment
kernel.
- A new describe pins the single-kernel reading per provider: for
`'env_1'`, `undefined` and `'platform'`, each of the five resolves
`ctx.getService(slot)` and makes exactly that one call.
## Acceptance notes
- **Changed answer, analytics only.** An environment whose kernel
carries no analytics service, or whose kernel cannot be acquired, now
gets the route's 501 where it used to get an answer from the host's
service.
- **Faults keep their existing route answers.** A provider rejection (a
kernel that cannot be acquired, a factory that threw) is caught by each
route's existing `resolveService` catch, as before, so it answers that
door's "not configured" shape (501, `{ data: [] }` or the empty record
page part) rather than a 5xx. That is the routes' pre-existing contract
and is not changed here. A wedged environment fails first in
`resolveExecCtx` on these doors.
- **Scope.** The plugin's other provider seams (auth, objectql, i18n,
settings, metadata, security, tenancy, endpoint match) are outside this
card and are unchanged.
- **Cloud's re-read** of `areq_bcb78e81` on staging under cloud#3305 1f
follows a pin round that carries this, and is cloud's work, not this
PR's.
## Verification
- Full `@objectstack/rest` suite under `scripts/pm/os-verify-lock.sh`:
385 files, 7,237 passed, 332 skipped, `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/rest typecheck` (`tsc --noEmit` plus
`check:test-typecheck` over `tsconfig.test.json`): green, 0 errors,
nothing held in the debt ledger.
- eslint on all nine touched source and test files: clean.
- The 60 commands `node scripts/pm/dispatch-gates.mjs --commands`
derives for this diff, each run with its exit code recorded, reconciled
by `dispatch-gates --ran`: 59 exit 0, 1 not measured.
- `pnpm check:dual-build-cjs-loads` exited 3 (`PREREQUISITE NOT MET`):
it reads every package's built `dist/`, and this worktree built only the
`@objectstack/rest` closure. CI's whole-tree build runs it.
- `node scripts/check-plugin-teardown-shape.mjs --self-test` first
exited 3 because this clone is shallow and its pinned fixture commit was
missing; after fetching that commit it passed (48 cases).
- The text gates (closing-keyword parity, comment-mask adoption and
corpus, issue citations, drift comment, affected docs, changeset
no-major and empty-changeset, ADR-0087 registration, nul bytes),
`check:slot-lookup` and `check:route-envelope` were re-run on the final
head `f1334056`: all exit 0.
- `node scripts/pm/dispatch-gates.mjs --tier`: no path-derived tier
mandate; 861 changed lines, under the human-merge threshold.
<details>
<summary>Ablation: the hosted-shape test against the base provider and
route sources</summary>
With `rest-api-plugin.ts`, the five route modules and `rest-server.ts`
restored to `73a3ef9f` and the new test file kept, the hosted describe
reads **39 failed, 5 passed (44)**.
- The 5 that pass are the four analytics cases with the environment
present (the route already read the environment's kernel first) and the
cold-build case.
- The four analytics `env_bare` cases fail: the base route answered them
from the host's service.
- Every approvals, sharing, sharing-rule, email and record page case
fails.
With the fix the same file reads 58 passed (58). The single-kernel pins
(the new describe and the updated slot-lookup file) pass on both trees,
15 run in the base-source run, which is the "unchanged" half.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QUs3BvMSSmip92aFsGuLjx
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QUs3BvMSSmip92aFsGuLjx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9d1a465 commit 63b86b3
10 files changed
Lines changed: 765 additions & 95 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
69 | | - | |
70 | | - | |
| 70 | + | |
| 71 | + | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
74 | | - | |
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
98 | 104 | | |
99 | 105 | | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
| 106 | + | |
| 107 | + | |
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| |||
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
105 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
106 | 113 | | |
107 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
108 | 118 | | |
109 | 119 | | |
110 | 120 | | |
| |||
236 | 246 | | |
237 | 247 | | |
238 | 248 | | |
239 | | - | |
| 249 | + | |
240 | 250 | | |
241 | 251 | | |
242 | 252 | | |
| |||
332 | 342 | | |
333 | 343 | | |
334 | 344 | | |
335 | | - | |
| 345 | + | |
336 | 346 | | |
337 | 347 | | |
338 | 348 | | |
| |||
362 | 372 | | |
363 | 373 | | |
364 | 374 | | |
365 | | - | |
| 375 | + | |
366 | 376 | | |
367 | 377 | | |
368 | 378 | | |
| |||
401 | 411 | | |
402 | 412 | | |
403 | 413 | | |
404 | | - | |
| 414 | + | |
405 | 415 | | |
406 | 416 | | |
407 | 417 | | |
| |||
441 | 451 | | |
442 | 452 | | |
443 | 453 | | |
444 | | - | |
| 454 | + | |
445 | 455 | | |
446 | 456 | | |
447 | 457 | | |
| |||
480 | 490 | | |
481 | 491 | | |
482 | 492 | | |
483 | | - | |
| 493 | + | |
484 | 494 | | |
485 | 495 | | |
486 | 496 | | |
| |||
529 | 539 | | |
530 | 540 | | |
531 | 541 | | |
532 | | - | |
| 542 | + | |
533 | 543 | | |
534 | 544 | | |
535 | 545 | | |
| |||
585 | 595 | | |
586 | 596 | | |
587 | 597 | | |
588 | | - | |
| 598 | + | |
589 | 599 | | |
590 | 600 | | |
591 | 601 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
189 | 189 | | |
190 | 190 | | |
191 | 191 | | |
| 192 | + | |
192 | 193 | | |
193 | 194 | | |
194 | 195 | | |
| |||
247 | 248 | | |
248 | 249 | | |
249 | 250 | | |
250 | | - | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
251 | 259 | | |
252 | 260 | | |
253 | 261 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| |||
429 | 430 | | |
430 | 431 | | |
431 | 432 | | |
432 | | - | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
433 | 441 | | |
434 | 442 | | |
435 | 443 | | |
| |||
0 commit comments