This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
Commit d99b731
feat(console): approval requests through the standard provider:'api' door (objectui#12032) (#12043)
Fixes #12032
Clause-②: no
A1 of the approvals rebuild (objectui#2763): approval requests now reach
the standard `ListView` and `RecordDetailView` through the existing
`provider: 'api'` door, with `viewer` and `decision_progress` as
ordinary fields. Built to the claim amendment on the card (comment
6077174661), which answered the dev report's three questions (comment
6077126267) with Q1 A, Q2 A and Q3 A.
## What changed
**`apps/console/src/services/approvalRequestsDataSource.ts` (new).**
`createApprovalRequestsDataSource({ host, scope })` returns a routed
`DataSource`, one per inbox scope:
| scope | list request |
|---|---|
| `awaiting_me` | `status=pending&approverId=` every identity,
comma-joined |
| `submitted_by_me` | `submitterId=` the user |
| `all` | no scope parameter |
- Reads of `sys_approval_request` go to `/approvals/requests` through
`ApiDataSource`. Every other resource and method goes to the host
adapter, untouched. `RecordDetailView` needs this, because it sends its
side reads (the `sys_user` directory on every mount, comments,
activities, history) through the same source.
- A scope that names no identity reads as an empty list and sends
nothing. It never falls through to the unscoped list.
- **List translation.** The list route has a closed parameter set and
answers any other name with a 400. So `$top` becomes `limit`, `$skip`
becomes `offset` and `$search` becomes `q`. `$select` is dropped: the
route has no projection, so dropping it widens columns, never rows.
Every other name (`$filter`, `$orderby`, `$expand`, `$searchFields`, or
anything unknown) is refused before any request with an
`UNSUPPORTED_QUERY_PARAM` error. `ListView` classifies that code as a
rejected query and shows its error panel, never unfiltered rows.
- **The get** goes through a door with no scope params and sends none of
the caller's `QueryParams`, because the route reads none. The id is
encoded into the path.
- Rows come back as the approvals service serves them. `viewer` is on
list and get rows. `decision_progress` is on the get only, which is the
contract: single-request reads of pending requests.
**`packages/core/src/adapters/ApiDataSource.ts`.** Only `findOne`'s
`catch` is narrowed. It answers `null` on a 404 and rethrows every other
failure (a 403, a 5xx, a transport error), as
`ObjectStackAdapter.findOne` does. The 404 is recognised from the
message the class's own `request` composes ("ApiDataSource: HTTP 404
…"), so no error shape changes. Before this, a refused or failed read
through any `provider: 'api'` source reached the record page as "Record
not found" instead of its "no access" or "could not load" state
(objectui#11902).
**`apps/console/src/services/approvalsApi.ts`.** `API_BASE` is now
exported, so the new module reaches the same routes without a second
spelling of where they live. This is the one helper the claim allows
sharing; nothing else in the file moved.
**`.changeset/12032-approval-requests-data-source.md`:** `patch` on
`@object-ui/core` and `@object-ui/console`.
There is no new package export, prop, registry type or pack key. Nothing
mounts the source yet; B1 and B2 will.
## Pins
`apps/console/src/services/approvalRequestsDataSource.test.tsx`, against
a fake approvals server. The fake restates the list route's closed set
verbatim (`APPROVAL_REQUEST_LIST_PARAMS` from objectstack's REST server)
and its bare-row get:
- each scope's list request, and the empty-identity case (no request,
empty result);
- the translation (`$top`, `$skip`, `$search`, with `$select` dropped
and `total` from the envelope), and the scope and paging riding
together;
- each of `$filter`, `$orderby`, `$expand` and `$searchFields` refused
before any request, with `code` `UNSUPPORTED_QUERY_PARAM` and the
parameter named;
- **through the real `ListView`:** a scoped list reads with `limit` and
no `$` name and draws the rows; the toolbar search reaches the route as
`q`; and a view that needs `$filter`, `$orderby`, `$expand` or
`$searchFields` shows the panel with `data-error-kind` `rejected`, draws
no rows and sends no request;
- the get, with `viewer` and `decision_progress` as fields and no query
on the wire; the id encoded; a 404 reading as `null`;
- the routed source: another resource's find and findOne go to the host,
writes and metadata go to the host, the host's other row reads are
refused for approval requests and are the host's for everything else,
and one identity per method across reads.
`apps/console/src/services/approvalRequestsDataSource.recordPage.test.tsx`
mounts the real `RecordDetailView` over the routed source, with only the
transport doubled:
- a 403 renders `record-access-denied` and never "Record not found";
- a 500 renders `record-load-failed` with Retry;
- controls: a 404 still renders "Record not found", and a found request
renders, read once from the get route with no query, while the page's
side reads go to the host.
`packages/core/src/adapters/__tests__/ApiDataSource.test.ts`: `findOne`
rejects on a 403, on a 500 and on a transport failure, beside the
unchanged "should return null on 404" pin.
## Reverse verification
Both runs used objectstack's `scripts/ablation-replace.mjs` in wrap
mode, from commit `db70859c0`. The anchor was hit once and the blob
change was proven on disk. Each restore was proven: blob equal to HEAD,
and `git diff HEAD` empty. Both runs resolve to source through the
vitest alias for `@object-ui/core`, so no build is in the path.
- **A, restore the catch-all in `findOne`** (`throw err;` replaced with
`return null;`). Predicted red: the three core rejection pins and the
record page's 403 and 500 pins. Observed: `Tests 5 failed | 32 passed
(37)`, exactly those five. The 404 control and the found-request control
stayed green.
- **B, drop the refusal of `$filter`** (`$filter` added to the dropped
set). Predicted red: the `$filter` unit refusal and the `ListView`
`$filter` panel pin. Observed: `Tests 2 failed | 22 passed (24)`,
exactly those two.
## Local gates (all on `db70859c0`, from the worktree root)
- `pnpm exec vitest run packages/core/`: `Test Files 198 passed (198)`,
`Tests 3939 passed | 27 skipped (3966)`.
- `pnpm exec vitest run apps/console/src/services/`: `Test Files 3
passed (3)`, `Tests 37 passed (37)`.
- The other packages' tests that construct `ApiDataSource` or
`resolveDataSource` (15 files across components, fields, plugin-detail,
plugin-gantt, plugin-kanban, plugin-list, plugin-view and react): `Test
Files 15 passed (15)`, `Tests 260 passed (260)`.
- `pnpm --filter @object-ui/core type-check` and `pnpm --filter
@object-ui/console type-check`: exit 0, after building the dependency
closure (`turbo run build --filter='@object-ui/console^...'`, 34 tasks
successful).
- `pnpm exec eslint` on the 6 touched files (count from `--format
json`): 0 errors. The 11 warnings are all `no-explicit-any` in
pre-existing lines; the new files have none. Type-aware linting is not
enabled in `eslint.config.js` (no `projectService` or
`parserOptions.project`), so this diff cannot move another file's
verdict.
- `check:control-bytes`, `check:test-path-roots`,
`check:changeset-claims`, `check:pending-changeset-literals`,
`check:new-line-citations`, `check:vi-mock-specifiers`,
`check:vi-mock-inherit`, `check:vi-mock-override-shape`,
`check:phantom-deps`, `check:unused-deps`, `check:self-import` and
`check:unreferenced-sources`: each exit 0. `node
scripts/check-changeset-presence.mjs`: "6 source file(s) of 2 released
package(s) changed, and this change declares 1 changeset(s)".
- NOT MEASURED: `check:eager-closure` and the first-load bytes, because
a console production build was out of reach locally; CI's Bundle
Analysis owns them. Nothing eager imports the new module. The core
change is a few lines inside one method.
## Acceptance notes
- **Two places this narrows "every other method goes to the host".** (1)
The host's other row reads (`aggregate`, `queryGroupHeaders`,
`exportDownload`) are refused when called for `sys_approval_request`,
with the same code. Forwarded, they would read approval requests through
the data API: outside the scope and without `viewer`. So a grouped grid
or a server export on these lists fails loudly instead of widening rows.
(2) The get sends none of the caller's `QueryParams`, so the record
page's `$expand` (it asks for every declared relation) is not sent. The
route has no expansion and serves its own display names
(`submitter_name`, `record_title`, …). Refusing `$expand` on the get
would break every request page, because `sys_approval_request` declares
lookups (`submitter_id`, `organization_id`). The seat may reverse
either; each is pinned.
- **For B2:** the list views plugin-approvals ships for
`sys_approval_request` all declare `sort` and `filter`, and most show
the `submitter_id` lookup. Mounted on this source, each would be refused
(`$orderby`, `$filter`, `$expand`), as ruled. B2 authors views without
them (scope comes from the source; use `submitter_name`), or a card with
a pull takes the route-side option to the spec seat.
- **For B2:** list rows carry `viewer` but never `decision_progress`
(contract), and `ListView` honours a `schema.data` `provider: 'api'`
config on the gantt view only. Both are recorded in the amendment.
- The core README's `ApiDataSource` section states nothing about
`findOne`'s failure semantics, so none of its text became false. It was
left alone because it is outside the claim's surface.
- The PM's original ablation list included "drop the scope"; the
amendment's list does not, and it was not run. The scope pins assert the
exact URL for each scope.
Session: `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`
---
_Generated by [Claude
Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 47b1f0b commit d99b731
7 files changed
Lines changed: 732 additions & 3 deletions
File tree
- .changeset
- apps/console/src/services
- packages/core/src/adapters
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
0 commit comments