Skip to content

Commit f57e9d5

Browse files
os-trumpclaude
andauthored
feat(spec): export the object-level refinement checks the mirrored UI schemas run (#16489) (#16697)
* feat(spec): export the object-level refinement checks the mirrored UI schemas run Every spec object that carries an object-level refinement and is mirrored downstream by a .shape derivation now exports its check as a named, typed function from the same module, one per refinement: checkListViewPageMount and checkListViewCalendarVisualization (ListViewSchema), checkPageSourceCompleteness (PageSchema, extracted from the inline superRefine), and checkGlobalFilterDateDefaultValue (GlobalFilterSchema, extracted likewise). Every schema attaches the export by identifier, so no accept set moves. object-refinement-check-exports.test.ts pins, per export: parity over every failure path between the direct call, the schema's own check object and the schema's parse; check count + bijection; attachment by identifier; barrel identity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf * test(spec): count refinement attachments in code only; regenerate api-surface + export-origins for the four exports Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf * test(spec): count refinement attachments by code line, no private comment stripper Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ecee668 commit f57e9d5

7 files changed

Lines changed: 538 additions & 53 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): export the object-level refinement checks the mirrored UI schemas run — `checkListViewPageMount`, `checkListViewCalendarVisualization`, `checkPageSourceCompleteness`, `checkGlobalFilterDateDefaultValue` (#16489, the spec half of objectui#7715)
6+
7+
objectui derives its zod schemas from the spec's `.shape` (`specFieldsExcept(SpecListViewSchema.shape, …)`, six sites at the pinned build). That carries the spec's FIELDS by reference and drops every check attached to the spec OBJECT (`superRefine` / `refine`), so at 17.3.0 objectui's authoring door accepted `appearance.allowedVisualizations: ['calendar']` with no `calendar:` block while the spec's publish door refused it. Contract-first: the rule is written once, in the spec, and a mirror attaches that same rule instead of re-implementing it.
8+
9+
Every spec object that carries an object-level refinement and is mirrored downstream now exports its check as a named, typed function (`(value, ctx: z.RefinementCtx) => void`) from `@objectstack/spec/ui`, alongside the schema — one function per refinement, no bundled "all checks" blob, so a mirror attaches exactly the ones whose fields it carries:
10+
11+
| Schema | Export | Refuses |
12+
|:--|:--|:--|
13+
| `ListViewSchema` | `checkListViewPageMount` | `type: 'page'` with no `pageName`; `pageName` on a view that is not `type: 'page'`; a page mount declaring `columns` |
14+
| `ListViewSchema` | `checkListViewCalendarVisualization` | `'calendar'` in `appearance.allowedVisualizations` with no `calendar:` block |
15+
| `PageSchema` | `checkPageSourceCompleteness` | an `html` / `react` / `jsx` page with no non-empty `source` |
16+
| `GlobalFilterSchema` | `checkGlobalFilterDateDefaultValue` | a `type: 'date'` filter whose `defaultValue` is neither a preset name, an ISO date, nor a date-macro token |
17+
18+
The population is measured from the schemas themselves (`_zod.def.checks`) against the six objectui derivation sites: `ListViewSchema` (2 checks), `PageSchema` (1) and `GlobalFilterSchema` (1 — mirrored by a `.shape` spread rather than `specFieldsExcept`) carry object-level refinements; `NavigationAreaSchema`, `AppSchema`, `DashboardWidgetSchema` and `DashboardSchema` carry none, so nothing is exported for them. The two `ListViewSchema` checks were already named module-private functions and are now exported; the `PageSchema` and `GlobalFilterSchema` checks were inline `superRefine` bodies, extracted verbatim into named functions the schema now attaches by identifier.
19+
20+
Additive, and the schemas are unchanged: every schema attaches the very function it exports, so no accept set moves — every ListView, Page and GlobalFilter document that parsed before parses identically, with identical issues. `minor` because four new symbols land in the published `dist/*.d.ts`. Each export is pinned to be the check its schema runs (`object-refinement-check-exports.test.ts`: parity over every failure path between the direct call, the schema's own check object and the schema's parse; the schema carries exactly as many checks as are exported for it; the module attaches each by identifier). Attaching them at the derivation sites is the objectui half, objectui#7715.

packages/spec/api-surface/ui.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,10 @@
429429
"chartAggregateCategoryKey (function)",
430430
"chartAggregateResultKeys (function)",
431431
"chartAggregateValueKey (function)",
432+
"checkGlobalFilterDateDefaultValue (function)",
433+
"checkListViewCalendarVisualization (function)",
434+
"checkListViewPageMount (function)",
435+
"checkPageSourceCompleteness (function)",
432436
"columnSummaryAlias (function)",
433437
"compileListViewGroupQuery (function)",
434438
"compileListViewGroupRowsQuery (function)",

packages/spec/export-origins/ui.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,10 @@
415415
"chartAggregateCategoryKey": "src/ui/chart-aggregate.ts#chartAggregateCategoryKey (function)",
416416
"chartAggregateResultKeys": "src/ui/chart-aggregate.ts#chartAggregateResultKeys (function)",
417417
"chartAggregateValueKey": "src/ui/chart-aggregate.ts#chartAggregateValueKey (function)",
418+
"checkGlobalFilterDateDefaultValue": "src/ui/dashboard.zod.ts#checkGlobalFilterDateDefaultValue (function)",
419+
"checkListViewCalendarVisualization": "src/ui/view.zod.ts#checkListViewCalendarVisualization (function)",
420+
"checkListViewPageMount": "src/ui/view.zod.ts#checkListViewPageMount (function)",
421+
"checkPageSourceCompleteness": "src/ui/page.zod.ts#checkPageSourceCompleteness (function)",
418422
"columnSummaryAlias": "src/ui/view-grouping-query.ts#columnSummaryAlias (function)",
419423
"compileListViewGroupQuery": "src/ui/view-grouping-query.ts#compileListViewGroupQuery (function)",
420424
"compileListViewGroupRowsQuery": "src/ui/view-grouping-query.ts#compileListViewGroupRowsQuery (function)",

packages/spec/src/ui/dashboard.zod.ts

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,63 @@ function isDateMacroPlaceholder(value: string): boolean {
698698
return !!m && isDateMacroToken(m[1]);
699699
}
700700

701+
/**
702+
* [#4614] The `type: 'date'` ⇄ `defaultValue` vocabulary check attached to
703+
* {@link GlobalFilterSchema} — a date filter's default is checked against the
704+
* vocabulary that can actually resolve it.
705+
*
706+
* Why this filter type and not the built-in `dateRange`: `dateRange`'s
707+
* `defaultRange` has always been an enum, so a typo there was already an
708+
* author-time error. A `globalFilters` entry of `type: 'date'` was the
709+
* asymmetric half — `defaultValue` is `string | number | boolean`, so a bare
710+
* preset name is the ONLY spelling available, and nothing checked it. An
711+
* unknown name then failed SILENTLY and late: the renderer cannot lift it to a
712+
* range, falls through to "a bare string date means equality on that day", and
713+
* emits `created_at = 'last_7_dayz'` — a condition no row matches, which the
714+
* backend answers `200 OK` with a zero. Every tile reads 0 and the filter bar
715+
* shows "All time", so the dashboard looks deliberately empty rather than
716+
* misconfigured. That is the failure this moves to parse time.
717+
*
718+
* Exported (#16489, the spec half of objectui#7715) so a downstream mirror
719+
* that spreads `GlobalFilterSchema.shape` — which carries the FIELDS by
720+
* reference and drops every object-level check — can re-attach exactly this
721+
* rule with `.superRefine(checkGlobalFilterDateDefaultValue)` instead of
722+
* re-parsing through the spec schema or re-implementing it. One function per
723+
* refinement, no bundle. `GlobalFilterSchema` attaches this same binding, so
724+
* the export IS the check the schema runs — pinned in
725+
* `object-refinement-check-exports.test.ts`.
726+
*/
727+
export function checkGlobalFilterDateDefaultValue(
728+
filter: { type?: string; defaultValue?: unknown },
729+
ctx: z.RefinementCtx,
730+
): void {
731+
if (filter.type !== 'date' || filter.defaultValue === undefined) return;
732+
733+
const value = filter.defaultValue;
734+
if (
735+
typeof value === 'string' &&
736+
((DATE_RANGE_PRESETS as readonly string[]).includes(value) ||
737+
isDateMacroPlaceholder(value) ||
738+
ISO_DATE_RE.test(value))
739+
) {
740+
return;
741+
}
742+
743+
ctx.addIssue({
744+
code: 'custom',
745+
path: ['defaultValue'],
746+
message:
747+
`${JSON.stringify(value)} is not a value a \`type: 'date'\` filter can resolve. ` +
748+
'Use one of three spellings: a preset name (' +
749+
DATE_RANGE_PRESETS.join(', ') +
750+
'); an ISO date such as `2026-01-15` or `2026-01-15T08:30:00Z`, meaning that ' +
751+
'day exactly; or a date-macro token such as `{today}` or `{30_days_ago}` ' +
752+
'(the full vocabulary is `DATE_MACRO_TOKENS` in `@objectstack/spec/data`). ' +
753+
"`custom` is not among them — it is a `dateRange.defaultRange` sentinel that " +
754+
'carries no bounds of its own.',
755+
});
756+
}
757+
701758
/**
702759
* Dynamic options binding for global filters.
703760
* Allows dropdown options to be fetched from an object at runtime.
@@ -791,47 +848,11 @@ export const GlobalFilterSchema = lazySchema(() => strictObject({
791848

792849
/** Widget IDs to apply this filter to (when scope is widget) */
793850
targetWidgets: z.array(z.string()).optional().describe('Widget IDs to apply this filter to'),
794-
}).superRefine((filter, ctx) => {
795-
// #4614 — a date filter's `defaultValue` is checked against the vocabulary
796-
// that can actually resolve it.
797-
//
798-
// Why this filter type and not the built-in `dateRange`: `dateRange`'s
799-
// `defaultRange` has always been an enum, so a typo there was already an
800-
// author-time error. A `globalFilters` entry of `type: 'date'` was the
801-
// asymmetric half — `defaultValue` is `string | number | boolean`, so a bare
802-
// preset name is the ONLY spelling available, and nothing checked it. An
803-
// unknown name then failed SILENTLY and late: the renderer cannot lift it to a
804-
// range, falls through to "a bare string date means equality on that day", and
805-
// emits `created_at = 'last_7_dayz'` — a condition no row matches, which the
806-
// backend answers `200 OK` with a zero. Every tile reads 0 and the filter bar
807-
// shows "All time", so the dashboard looks deliberately empty rather than
808-
// misconfigured. That is the failure this moves to parse time.
809-
if (filter.type !== 'date' || filter.defaultValue === undefined) return;
810-
811-
const value = filter.defaultValue;
812-
if (
813-
typeof value === 'string' &&
814-
((DATE_RANGE_PRESETS as readonly string[]).includes(value) ||
815-
isDateMacroPlaceholder(value) ||
816-
ISO_DATE_RE.test(value))
817-
) {
818-
return;
819-
}
820-
821-
ctx.addIssue({
822-
code: 'custom',
823-
path: ['defaultValue'],
824-
message:
825-
`${JSON.stringify(value)} is not a value a \`type: 'date'\` filter can resolve. ` +
826-
'Use one of three spellings: a preset name (' +
827-
DATE_RANGE_PRESETS.join(', ') +
828-
'); an ISO date such as `2026-01-15` or `2026-01-15T08:30:00Z`, meaning that ' +
829-
'day exactly; or a date-macro token such as `{today}` or `{30_days_ago}` ' +
830-
'(the full vocabulary is `DATE_MACRO_TOKENS` in `@objectstack/spec/data`). ' +
831-
"`custom` is not among them — it is a `dateRange.defaultRange` sentinel that " +
832-
'carries no bounds of its own.',
833-
});
834-
}));
851+
})
852+
// #4614 — the date `defaultValue` vocabulary check. Attached by identifier,
853+
// not inlined: the export is the rule a `.shape` mirror re-attaches (#16489),
854+
// and it must be this binding, not a copy.
855+
.superRefine(checkGlobalFilterDateDefaultValue));
835856

836857
/**
837858
* Dashboard Schema

0 commit comments

Comments
 (0)