fix(plugin-form): a required field with a runtime defaultValue is submittable on create (#4069) - #4084
Merged
Conversation
…ubmittable on create (#4069) `@objectstack/spec` lets `defaultValue` be a runtime instruction rather than a value — the `DEFAULT_VALUE_TOKENS` family (`NOW()` / `current_user`) or a CEL Expression envelope — which `ObjectQL.applyFieldDefaults` resolves per insert for any field arriving absent or null. #4068 therefore leaves such fields empty in a create form: seeding the literal text `NOW()` and submitting it would suppress the very resolution the declaration asked for. Correct for an optional field; combined with `required: true` it deadlocked. The control opened empty, the client-side required rule refused the submit, and there was nothing sensible for the user to type. Measured on origin/main: `dataSource.create` was never called, on both the flat and sectioned paths. Per the maintainer's 2026-08-10 ruling on #4069 (option A), in CREATE mode a runtime `defaultValue` now suppresses the client-side required rule and the field is omitted from the payload — omitted, not sent empty, because a rendered control registers regardless of seeding and would otherwise carry `undefined` (a key a data source may still write) or `''` (neither absent nor null, so it stores a blank and defeats the declaration). Seeding and the required rule read ONE predicate, `isRuntimeDefault`, so a form can never seed a field it also refuses to submit. Edit mode, static literal defaults and typed values are unchanged, each pinned in both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4069
The defect
@objectstack/speclets a field'sdefaultValuebe a runtime instructionrather than a value — the
DEFAULT_VALUE_TOKENSfamily ('NOW()','current_user') or a CEL Expression envelope{ dialect, source }. The serverresolves those per insert, in
ObjectQL.applyFieldDefaults, for any field thatarrives absent or null. #4068 therefore has every object-form container leave
such a field empty: seeding the literal text
NOW()into a datetime input andsubmitting it as the field's value would suppress the very resolution the
declaration asked for.
Correct for an optional field. Combined with
required: trueit deadlocked:the control opened empty, the client-side required rule refused the submit, and
there was nothing sensible for the user to type — the declaration had already
said what the value is, and omitting the field is exactly what makes the server
supply it.
Premise verified against
origin/mainbefore implementing (issue bodies areleads). A probe reproducing the issue's own recipe — make the runtime-token
field in
createDefaults.test.tsxrequired: trueand submit — measureddataSource.createnever being called, on both paths:The ruling this implements
Maintainer, 2026-08-10 (issue #4069) — Option A now; B may layer on later; C
and D rejected:
No framework or server changes (C rejected), and no lint refusal (D rejected —
required: true+ a runtime default is coherent authoring, storage-levelrequired with a producer-guaranteed value). The resolved preview (B) is a
separate follow-up card and is deliberately not started here.
What changed
One classifier, two consumers. #4068 introduced the runtime-default test
inside
isSeedableDefaultinschemaDefaults.ts. Rather than write a secondone, that predicate is lifted out as
isRuntimeDefaultandisSeedableDefaultis re-expressed in terms of it. Seeding and the required rule are the same fact
seen twice — a field whose value the producer supplies is neither seedable nor
missing — and a second copy would be free to disagree about, say, a CEL
envelope, at which point a form would seed a field it also refuses to submit.
Two levers, both gated on create:
isRequiredInForm(field, isCreateForm)replaces the fourrequired: field.required || falsesites that derive a runtime form fieldfrom the object schema —
ObjectForm,sectionFields(which serves Modal /Drawer / Tabbed / Split / Wizard, and the wizard's own cross-step gate), and
the no-sections paths in
ModalForm/DrawerForm. It is also re-appliedover a form view's
requiredoverride, because what excuses the field is theruntime
defaultValueon the object field, not which layer assertedrequired.omitServerResolvedDefaults(payload, objectSchema)in each container'screate branch.
Why the second lever is not optional. Suppressing the rule alone leaves half
the bug. A rendered control registers with the form whether or not anything
seeded it, so an untouched runtime-default field still reaches the payload as
undefined— or as''once anything focuses it.undefinedis invisible to aJSON.stringifyinspection (it cost this PR one lap: an early probe printed aclean-looking payload while the keys were in fact present) yet is still a KEY a
data source may translate into an explicit column write; and
''is neitherabsent nor null, so it stores a blank and defeats the declaration outright.
Emptiness is
isMissingForRequired, the same predicate the required rule uses,so "left empty" cannot come to mean two different things in the two halves.
isCreateFormModeis the single "no persisted record" test, now shared by theseeding gate and this one so they cannot drift.
Boundaries, each pinned in both directions
required+ a runtime defaultrequired+ a runtime defaultrequired, no defaultrequired+ a static literalrequired, anythingThe runtime shapes in the fixtures come from
@objectstack/spec's ownDEFAULT_VALUE_TOKENSrather than a hand-copied list, so a token added to thefamily tomorrow is covered without editing the suite, and the fixture cannot
drift from the classifier.
Deliberate consequence: the required marker and
aria-requiredgo with therule in the create case, since one boolean drives all three. That is the honest
reading — in create mode the user really is not required to provide the value —
and it removes the "required marker over an input the user may skip" oddity the
filer flagged under option A. Surfacing what the server will supply is option
B's job.
Not extended to
requiredWhen, the conditional-required CEL rule: it isresolved downstream in the form renderer against the live record, outside this
package's surface. Filed separately as an observation-class finding rather than
widened here.
Verification
Reverse verification, direction predicted before running: the create-mode pins
must go red and the two boundary pins (static-literal cleared, edit blanked)
must stay green, since they describe unchanged behavior. Reverting only the
behavioral levers to
origin/main— the seven container/sectionFieldsfiles,keeping
schemaDefaultsso the classifier stayed importable — gave exactlythat: 13 failed | 41 passed, with every failure in the create-mode group and
both boundary pins plus the pure-predicate block still green. Restored via a
patch file, never
git stash.Downstream consumer sweep — prefix filter
'...@object-ui/plugin-form',i.e. the packages that consume it, after building each closure first:
apps/siteand the three examples declare no test script.plugin-designerfirst failed to resolve
@object-ui/mobilefromplugin-grid; that is thestale-artefact trap, not this change — building its dependency closure turned it
green with the change still applied.
Release
patchon@object-ui/plugin-form: a behavior change to a released package'screate-form semantics, with no new exported API —
schemaDefaultsandsectionFieldsare internal modules, absent from the packageindex. The onlysignature widening is an optional
recordIdon the internalSectionFieldsContext. Happy to let the Bump Policy gate arbitrate.Generated by Claude Code