Skip to content

fix(plugin-form): decline to fetch a detail collection with no child object - #6191

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5940-detail-collection-decline-to-fetch
Aug 25, 2026
Merged

fix(plugin-form): decline to fetch a detail collection with no child object#6191
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5940-detail-collection-decline-to-fetch

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Fixes#5940

object-master-detail-form called getObjectSchema(d.childObject) with no guard on the key being resolved, so a malformed detail entry made the block ask the data layer for an object literally named undefined.

The discriminating step the card asked for

The card left both of its halves open and named the measurement that decides the second. Both readings, taken from the source rather than from the sample:

1. Which key does the renderer read the child object off? Neither .object nor .objectName — it is childObject, at MasterDetailForm.tsx:341 (pre-fix numbering):

const childSchema = await dataSource.getObjectSchema(d.childObject);

2. What is a details entry declared to be? An object with a required child-object key — not a bare string. MasterDetailFormSchema.details is MasterDetailDetailConfig[], and that interface opens with a non-optional childObject: string ("Child object name, e.g. 'expense_line'"). Every other key on the entry is optional and derived from the child's metadata. It is what all three downstream reads are keyed on: deriveDetail(d.childObject, …), the child-schema cache, and each child fetch's FK scope.

So the fixture half is taken: ['name'] is not a detail collection any author could publish, and the probe's sample table is corrected — on the declared shape, not on the bad call going away (the #3840 discriminator).

The fix, and the precedent it follows

The resolve effect now skips an entry whose childObject is unresolved and warns, leaving the entry in place — exactly what the adjacent catch already does, so the grid card still shows its config hint and details stays length-matched to rawDetails (the row-state array is indexed against it).

This is the decline RelatedList already makes for the same class of missing key — "has no referenceField/parentId — refusing to fetch all rows" (RelatedList.tsx:498), whose schema fetch is likewise gated if (api && dataSource?.getObjectSchema). The sibling effect in this same component, the child-schema cache, already spelled it .filter(Boolean). The three now agree rather than failing three ways.

What was measured, each direction predicted before running

Verified at fd8df8399, root vitest (objectui#3378), against the #3840 binding-reach fixture with a call-recording Proxy data source of the same shape as the probe's.

Before — predicted two calls, second getObjectSchema(undefined). Reproduced exactly:

["getObjectSchema(\"probe_object__c\")", "getObjectSchema(undefined)"]

After — predicted the second absent and the first still made. Both hold:

["getObjectSchema(\"probe_object__c\")"]

⭐ Both halves are asserted, because a "fix" that declined to fetch everything would also make the bad call vanish and would pass an absence-only test. The regression test pins the exact call list, and a second test independently proves a well-formed { childObject: 'invoice_line' } still fetches.

Reverse verification. The fix was committed first, then reverted alone (git checkout HEAD~1 -- MasterDetailForm.tsx) under a restore trap. Mutation confirmed on disk by grepping the text intended to be removed (if (!d.childObject) → 0 occurrences, the warn string → 0), not by the edit tool's exit code. Predicted red on test 1 only; observed exactly that:

× declines to fetch instead of calling getObjectSchema(undefined)
AssertionError: expected [ …(2) ] to deeply equal [ Array(1) ]
Tests 1 failed | 1 passed (2)

Test 2 stayed green, which is correct — a well-formed detail was always fetched, so it does not depend on the guard. The restore leg was confirmed byte-identical (git status empty against the commit).

Note this call is invisible to the binding-reach probe's assertions: that probe asks whether any call carried the object name, and the first, correct call already satisfies it. The probe was green for as long as the defect was live, so its green is not evidence for this fix — hence an assertion that reads the full call list.

Gates

Each quoted from the gate's own verdict line; exit codes captured by redirect before any pipe.

  • pnpm lint (full repo, eslint . --no-inline-config) — Tasks: 47 successful, 47 total, 0 errors. Run in full under the shared verify lock (VERDICT command-exit 0 · held the lock 178s), so no narrowing is claimed.
  • pnpm --filter @object-ui/plugin-form type-check — exit 0 (tsc --noEmit && tsc -p tsconfig.test.json), after building the dependency closure.
  • pnpm --filter @object-ui/console type-checkVERDICT command-exit 0.
  • packages/plugin-formTest Files 61 passed (61), Tests 615 passed (615).
  • Union on the final commit fd8df8399: plugin-form + both reach probes — Test Files 63 passed (63), Tests 644 passed (644).
  • public-block-binding-reach after the sample change — Tests 16 passed (16).
  • Sibling parity suites (record-block-record-reach, registry-inputs-spec-parity, masterDetailFormTypeManifest, public-contract, catalog-gallery-render) — Tests 672 passed (672).
  • pnpm check:control-bytesOK (scanned 5110 tracked text file(s)).
  • pnpm changeset:check — all three checks OK.

Branched from today's origin/main (b37d3f0), which already carries #6177; the formType enum it landed was read from the file as it stands, not quoted from before 22:58Z.

Scope

The renderer's detail-collection resolution, its test, and the probe's sample table. The sweep turned up one same-family site left untouched and filed instead: #6188record:line_items (LineItemsPanel.tsx:114) reads schema.childObject through the identical unguarded path. That card is out of scope here and #3838 is not addressed here.


Generated by Claude Code

…object
`object-master-detail-form`'s detail resolve effect called
`getObjectSchema(d.childObject)` with no guard on the key being resolved. A
malformed detail entry therefore made the block ask the data layer for an object
literally named `undefined` — a real backend receives that query and whatever it
returns becomes the console's problem.
The effect now skips such an entry and warns, leaving it in place so the grid
card shows its config hint and the row-state array stays index-matched. This is
the choice `RelatedList` already makes for the same class of missing key, and
the sibling child-schema-cache effect in this same component already spelled it
`.filter(Boolean)`.
The binding-reach probe's generic array sample left `details` as `['name']`,
which the declared entry shape (`MasterDetailDetailConfig.childObject` is
required) rules out as a publishable configuration; the sample is corrected on
that reading, not on the bad call going away.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3221.2 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-CkDtppwq.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.15KB114.53KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)168.48KB46.47KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.14KB61.94KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.86KB30.71KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.05KB54.38KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghui-tw
yinlianghui-tw marked this pull request as ready for review August 24, 2026 23:52
@yinlianghui-tw
yinlianghui-tw added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 17ccec9Aug 25, 2026
25 checks passed
@yinlianghui-tw
yinlianghui-tw deleted the claude/issue-5940-detail-collection-decline-to-fetch branch August 25, 2026 00:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui-tw@claude