Skip to content

feat(console): approval requests through the standard provider:'api' door (objectui#12032) - #12043

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12032-approvals-datasource
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12032-approvals-datasource

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

claude added 2 commits October 9, 2026 08:30
…door (objectui#12032)

A1 of objectui#2763. A routed DataSource, one per inbox scope: reads of
sys_approval_request go to the approvals routes through ApiDataSource, and
every other resource and method to the console's own adapter. Rows come
back as the approvals service serves them, so viewer and decision_progress
are fields the standard ListView and RecordDetailView bind.

- The list route has a closed parameter set: $top, $skip and $search become
  limit, offset and q; $select is dropped; every other name is refused with
  UNSUPPORTED_QUERY_PARAM before any request, so ListView shows its error
  panel rather than unscoped rows.
- The get sends no scope and no QueryParams (the route reads none).
- ApiDataSource.findOne resolves null only on a 404 and rejects every other
  failure, so the record page tells no-access and load-failed from
  not-found (objectui#11902 states).
- approvalsApi.ts exports API_BASE so both reach the same routes.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…trictly (objectui#12032)

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 289 chunks) 3162.6 KB 3204.6 KB
Main entry chunk (gzip) 71.6 KB 350 KB
Entry file index-DDsmA_fS.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.52KB 7.19KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.83KB 141.44KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 268.74KB 68.04KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.62KB 69.16KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.25KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.20KB 23.04KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 09:09
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 09:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit d99b731 Oct 9, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12032-approvals-datasource branch October 9, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants