Skip to content

spec rc.6 refines a date GlobalFilter.defaultValue to reject { preset } — the exact shape objectui's normalizer produces and stored dashboards carry #4165

Description

@yinlianghui

A producer/consumer contract conflict surfaced by the @objectstack/spec 17.0.0-rc.6 bump in objectstack#7100. Filed as a decision, not implemented — the deciding half belongs to the spec owner, and encoding either answer consumer-side would preempt it.

What changed

rc.6 added a cross-field refinement to GlobalFilterSchema.

Measuring it correctly matters, and the obvious probe is misleading — parsing a { preset } default against the bare spec schema DOES fail, but on the OLD field type (defaultValue is string | number | boolean, invalid_union, message Invalid input), and the refinement never runs. Isolate it by widening the field first — which is exactly the shape objectui's dialect has:

const widened = SpecGlobalFilterSchema.safeExtend({ defaultValue: z.any().optional() });
widened.safeParse({ field: 'created_at', type: 'date', defaultValue: { preset: 'last_7_days' } })

That is refused, and the message names the rule:

{"preset":"last_7_days"} is not a value a type: 'date' filter can resolve. Use one of three spellings: a preset name (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days); an ISO date such as 2026-01-15 or 2026-01-15T08:30:00Z, meaning that day exactly; or a date-macro token such as {today} or {30_days_ago} (the full vocabulary is DATE_MACRO_TOKENS in @objectstack/spec/data). custom is not among them — it is a dateRange.defaultRange sentinel that carries no bounds of its own.

The bare preset NAME (defaultValue: 'last_7_days') is accepted by both sides.

Why it bites objectui specifically

@object-ui/types' GlobalFilterSchema derives from the spec's and pins three deliberate divergences (packages/types/src/zod/complex.zod.ts) — the widened keys are options, optionsFrom and defaultValue. The third is this one:

defaultValue stays z.any()normalizeDateDefault (framework#4475) lifts a date preset NAME into { preset }, and stored dashboards carry that object form, which the spec's string | number | boolean rejects.

So objectui does not merely tolerate { preset }: @object-ui/core's dashboard-filters.ts produces it, and it is what is on disk.

The evidence chain, end to end:

  1. framework#4475 introduced normalizeDateDefault, which lifts a preset name into { preset }.
  2. @object-ui/core/src/utils/dashboard-filters.ts runs it, so the object form is what objectui writes.
  3. packages/types/src/zod/complex.zod.ts widened defaultValue to z.any() for exactly that reason, and said so in prose.
  4. packages/types/src/__tests__/report-chart-query-spec-parity.test.tsaccepts the normalized date-preset default object has pinned it since.

Widening the FIELD used to be enough, because nothing else in the spec had an opinion. A refinement is not a field type, so it no longer is: any derivation that carries the refinement refuses the object form however defaultValue is typed.

The mechanical consequence, and what objectstack#7100's PR does

rc.6 closed both extension doors on this schema:

  • .extend() — what the code used to be — now throws at module load: "Cannot overwrite keys on object schemas containing refinements. Use .safeExtend() instead." It took six @object-ui/types suites down before any of them ran a test.
  • .safeExtend() — zod's own suggested replacement — runs, but is "safe" precisely in that it will not let you REPLACE an existing key's type. It types every incompatible override as never, so all three divergences stop compiling (TS2322 … is not assignable to type 'never').

Some spelling therefore had to change. PR #4169 composes by spreading SpecGlobalFilterSchema.shape — the spec's fields still flow in BY REFERENCE, the three keys are replaced, and the refinement is not carried. That is byte-for-byte the behaviour this schema had at rc.5, so the bump changes nothing here and neither answer below is encoded. A tripwire pins the standstill and goes red from either side (objectui stops accepting the object form, or the refinement is withdrawn/reworded upstream).

The options

A. Upstream relaxes in rc.7 — the refinement was not meant to exclude the stored { preset } form.

B. Upstream confirms intent{ preset } is deliberately not a resolvable date default.

  • Then objectui migrates: change normalizeDateDefault to keep the bare preset name, drop the defaultValue divergence (the other two are unaffected), adopt the refinement, and handle stored dashboards that already hold the object form.
  • Scope estimate: one function in @object-ui/core/src/utils/dashboard-filters.ts, the divergence and its prose in packages/types/src/zod/complex.zod.ts, the two pins in report-chart-query-spec-parity.test.ts, plus a read-path migration for stored dashboards (the part with real risk — until it runs, an existing dashboard fails validation). Small code, non-trivial data.
  • Expressively this costs nothing: the preset NAME is already one of the three legal spellings, so it is a rewrite, not a capability loss.

C. Keep the divergence permanently. Rejected on principle, and named only so it is visibly rejected: objectui would accept a dashboard the platform refuses — the designer stays green and the save fails server-side. That is the tolerant-consumer failure AGENTS.md #0.1 exists to prevent, one level up.

Recommendation: ask upstream first (A vs B), never C. A and B differ only in who moves, and the answer is a fact about the spec owner's intent that we do not hold. Consumer-side we take neither until then; the standstill costs nothing while it lasts, because the spread preserves rc.5 behaviour exactly.

Blocking

PR #4169 stays draft until this is ruled. It carries the global_nav retirement (objectstack#7100) plus the rc.6 bump, which cannot compile apart.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions