Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/9618-record-source-ladder-attribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/types': patch
'@object-ui/core': patch
---

The `ObjectGanttSchema` and `ObjectCalendarSchema` record-source text now names
the function their renderers actually call (objectui#9618).

Their zod `.describe` strings and TS docs said the `data` → `staticData` →
`objectName` ladder is resolved by `getDataConfig`. Neither renderer has had a
function by that name since the ladder moved into `@object-ui/core`'s shared
`resolveRecordSourceConfig` (objectui#7632), so the text now names that
function. The ladder order is unchanged, and so is every accepted document.
The `ObjectMapSchema` text still says `getDataConfig`, which is true:
`ObjectMap.tsx` keeps a local wrapper by that name that delegates to the shared
ladder. The `resolveRecordSourceConfig` docblock in `@object-ui/core` now says
that the text it quotes is the map faces' text.
20 changes: 13 additions & 7 deletions packages/core/src/utils/record-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import type { ViewData } from '@object-ui/types';
* more than one binding?** — the published three-rung record-source ladder
* (`data`, then `staticData`, then `objectName`), declared on both faces of
* the contract (`ObjectMapSchema.objectName` / `ObjectGanttSchema.objectName`
* in `@object-ui/types`, and the `.describe` on their zod twins:
* *"objectName — the THIRD record source `getDataConfig` resolves, after
* `data` and `staticData`"*), ruled objectui#6939 (2026-09-02) and pinned by
* in `@object-ui/types`, and the `.describe` on their zod twins — the map
* twin's reads *"objectName — the THIRD record source `getDataConfig`
* resolves, after `data` and `staticData`"*, naming `ObjectMap.tsx`'s local
* wrapper; the gantt twin names `resolveRecordSourceConfig` itself), ruled
* objectui#6939 (2026-09-02) and pinned by
* `objectql-record-source-refinement-6939.test.ts`. **That is this
* function.**
* 2. **How does `objectName` get POPULATED when it is absent?** — the
Expand All @@ -47,8 +49,9 @@ import type { ViewData } from '@object-ui/types';
* ladder reduces to two rungs — the resolved config's object when it names one,
* else the schema's own `objectName`, which is what a `value`/`api`-backed block
* still needs for metadata reads, i18n field labels and permission verdicts.
* Callers pass the ALREADY-RESOLVED config (their `getDataConfig(schema)`
* output), so rung ordering is settled before this function is reached.
* Callers pass the ALREADY-RESOLVED config (their
* {@link resolveRecordSourceConfig} output, direct or through a local wrapper),
* so rung ordering is settled before this function is reached.
*
* ## No lenient rung was added (AGENTS.md #0.1)
*
Expand All @@ -62,7 +65,7 @@ import type { ViewData } from '@object-ui/types';
*
* @param schema - The block's schema; only `objectName` is read.
* @param dataConfig - The RESOLVED data config — the caller's own
* `getDataConfig(schema)` output, `null` when nothing is bound.
* {@link resolveRecordSourceConfig} output, `null` when nothing is bound.
* @returns The bound object's name, or `undefined` when neither the resolved
* config nor the schema names one.
*
Expand Down Expand Up @@ -185,7 +188,10 @@ function authoredDataIsOnTheDeclaredArm(authored: unknown, arm: RecordSourceData
*
* `data`, then `staticData`, then `objectName` — declared on both faces of the
* published contract and pinned by
* `objectql-record-source-refinement-6939.test.ts`:
* `objectql-record-source-refinement-6939.test.ts`. Quoted here from the
* `ObjectMapSchema` faces, whose `getDataConfig` is `ObjectMap.tsx`'s local
* wrapper around this function; the gantt and calendar faces name
* `resolveRecordSourceConfig` instead (objectui#9618):
*
* 1. **`data`** — *"Data source configuration. Read FIRST by `getDataConfig`"*,
* honoured ONLY on the arm `dataArm` names. Returned verbatim, so a config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
* ## The defect
*
* Both mirrors REQUIRED `objectName`. Both renderers resolve their records from
* one of THREE keys, in this order — `getDataConfig` in
* `plugin-map/src/ObjectMap.tsx` (`schema.data`, `schema.staticData`,
* `schema.objectName`) and `plugin-gantt/src/ObjectGantt.tsx` (the same three,
* the same order). A document authored on `staticData` alone draws correctly
* one of THREE keys, in this order (`schema.data`, `schema.staticData`,
* `schema.objectName`) — today through the shared `resolveRecordSourceConfig`
* in `@object-ui/core` (objectui#7632), which `plugin-map/src/ObjectMap.tsx`
* reaches via its local `getDataConfig` wrapper and
* `plugin-gantt/src/ObjectGantt.tsx` calls directly. A document authored on
* `staticData` alone draws correctly
* and was refused by `safeValidateSchema`: six catalog entries, three per
* component, every one of them `staticData`-only.
*
Expand All @@ -43,7 +45,7 @@ import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ObjectGanttSchema, ObjectMapSchema, safeValidateSchema } from '../zod/index.zod';
import { ObjectCalendarSchema, ObjectGanttSchema, ObjectMapSchema, safeValidateSchema } from '../zod/index.zod';

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');

Expand Down Expand Up @@ -149,3 +151,46 @@ describe('objectui#6939 — the refinement sits on declared keys, and the object
expect(ObjectMapSchema.safeParse({ type: 'object-map', objectName: 'stores', data: 'nope' }).success).toBe(false);
});
});

describe('objectui#9618 — the record-source text names the function each renderer actually has', () => {
// `getDataConfig` left `ObjectGantt.tsx` (and never lived in
// `ObjectCalendar.tsx`) when objectui#7632 moved the ladder into
// `resolveRecordSourceConfig`; only `ObjectMap.tsx` keeps a local wrapper by
// that name. So the gantt and calendar faces must not name it, and the map
// face — whose text is TRUE — is the control that the descriptions and the
// source slice are actually being read.
type Described = { shape: Record<string, { description?: string }> };
const describes = (member: unknown): Record<string, string> =>
Object.fromEntries(
Object.entries((member as Described).shape).map(([k, v]) => [k, v.description ?? '']),
);

/** The `export interface NAME` block of the TS face, up to the next export. */
function tsFace(name: string): string {
const src = fs.readFileSync(path.join(REPO_ROOT, 'packages/types/src/objectql.ts'), 'utf8');
const at = src.indexOf(`export interface ${name} `);
expect(at, `${name} interface not found in objectql.ts`).toBeGreaterThan(-1);
const end = src.indexOf('\nexport ', at + 1);
return src.slice(at, end === -1 ? undefined : end);
}

it.each([
['ObjectGanttSchema', ObjectGanttSchema],
['ObjectCalendarSchema', ObjectCalendarSchema],
] as const)('%s: no zod describe and no TS doc names `getDataConfig`', (name, member) => {
const d = describes(member);
expect(Object.entries(d).filter(([, text]) => text.includes('getDataConfig'))).toEqual([]);
expect(d.objectName).toContain('resolveRecordSourceConfig');
expect(d.staticData).toContain('resolveRecordSourceConfig');
const face = tsFace(name);
expect(face).not.toContain('getDataConfig');
expect(face).toContain('resolveRecordSourceConfig');
});

it('⛔ CONTROL: the map faces still name its real local `getDataConfig`', () => {
expect(describes(ObjectMapSchema).objectName).toContain('getDataConfig');
expect(tsFace('ObjectMapSchema')).toContain('getDataConfig');
const renderer = fs.readFileSync(path.join(REPO_ROOT, 'packages/plugin-map/src/ObjectMap.tsx'), 'utf8');
expect(renderer).toContain('function getDataConfig(');
});
});
21 changes: 11 additions & 10 deletions packages/types/src/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2938,9 +2938,9 @@ export interface ObjectTreeSchema extends BaseSchema {
export interface ObjectGanttSchema extends BaseSchema {
type: 'object-gantt';
/**
* ObjectQL object name — the THIRD record source `getDataConfig` resolves,
* after {@link ObjectGanttSchema.data} and {@link ObjectGanttSchema.staticData}
* (`plugin-gantt/src/ObjectGantt.tsx`).
* ObjectQL object name — the THIRD record source `resolveRecordSourceConfig`
* (`@object-ui/core`) resolves, after {@link ObjectGanttSchema.data} and
* {@link ObjectGanttSchema.staticData} (called by `plugin-gantt/src/ObjectGantt.tsx`).
*
* Optional since objectui#6939: a gantt authored on inline rows never reads
* this key, and requiring it refused three catalog entries that draw
Expand All @@ -2951,8 +2951,9 @@ export interface ObjectGanttSchema extends BaseSchema {
*/
objectName?: string;
/**
* Data source configuration. Read FIRST by `getDataConfig` — `if
* (schema.data) return schema.data;` — ahead of `staticData` / `objectName`.
* Data source configuration. Read FIRST by `resolveRecordSourceConfig`
* (`@object-ui/core`), which returns it verbatim when it is on the block's
* declared arm — ahead of `staticData` / `objectName`.
*
* Declared by objectui#6939, in the same stroke as the mirror's `data`: until
* then the read landed on `BaseSchema`'s index signature on this side and
Expand Down Expand Up @@ -3242,7 +3243,7 @@ export interface ObjectGanttSchema extends BaseSchema {
// correct and is why they now have to be declared HERE. `plugin-gantt`'s
// registry mapping (`OBJECT_GANTT_DATA_SOURCE` in `index.tsx`) names `filter`
// and `sort` as the two keys the element data-source binding maps onto.
/** Inline records, wrapped into a `{ provider: 'value' }` config by `getDataConfig`. */
/** Inline records, wrapped into a `{ provider: 'value' }` config by `resolveRecordSourceConfig`. */
staticData?: any[];
/** Query filter (JSON Rules format), forwarded verbatim as `$filter`. */
filter?: any[];
Expand Down Expand Up @@ -3270,9 +3271,9 @@ export interface ObjectGanttSchema extends BaseSchema {
export interface ObjectCalendarSchema extends BaseSchema {
type: 'object-calendar';
/**
* ObjectQL object name — the THIRD record source `getDataConfig` resolves,
* after {@link ObjectCalendarSchema.data} and {@link ObjectCalendarSchema.staticData}
* (`plugin-calendar/src/ObjectCalendar.tsx`).
* ObjectQL object name — the THIRD record source `resolveRecordSourceConfig`
* (`@object-ui/core`) resolves, after {@link ObjectCalendarSchema.data} and
* {@link ObjectCalendarSchema.staticData} (called by `plugin-calendar/src/ObjectCalendar.tsx`).
*
* Optional since objectui#7313 (the objectui#6939 shape): a calendar authored
* on inline rows never reads this key, and requiring it refused the two
Expand Down Expand Up @@ -3325,7 +3326,7 @@ export interface ObjectCalendarSchema extends BaseSchema {
* — `ViewData` on both, deliberately kept.
*/
data?: SpecObjectCalendarProps['data'];
/** Inline records, wrapped into a `{ provider: 'value' }` config by `getDataConfig`. */
/** Inline records, wrapped into a `{ provider: 'value' }` config by `resolveRecordSourceConfig`. */
staticData?: any[];
/**
* The configuration container, and the FIRST thing this element's renderer
Expand Down
24 changes: 13 additions & 11 deletions packages/types/src/zod/objectql.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1716,16 +1716,17 @@ export const ObjectMapConfigSchema = z.object({
*
* Those renderers resolve their records from ONE of three keys, in this order:
* `data`, `staticData` (inline rows, wrapped into a `{ provider: 'value' }`
* config) or `objectName` (the bound object) —
* `getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
* `plugin-gantt/src/ObjectGantt.tsx`, each `if (schema.data) … if
* (schema.staticData) … if (schema.objectName) … return null`. Both mirrors
* config) or `objectName` (the bound object) — the shared ladder
* `resolveRecordSourceConfig` in `@object-ui/core`, which
* `plugin-map/src/ObjectMap.tsx` reaches through its local `getDataConfig`
* wrapper and `plugin-gantt/src/ObjectGantt.tsx` calls directly: `data`, then
* `staticData`, then `objectName`, else `null`. Both mirrors
* used to REQUIRE `objectName` alone, so a document authored on `staticData`
* (6 of the 20 catalog entries objectui#6939 measured) drew correctly and was
* refused by `safeValidateSchema` — `declared !== enforced`, with the corpus
* on the right side. `objectName` is optional on both members now, and this
* refinement carries the requirement the renderers actually have: with none of
* the three present `getDataConfig` returns `null` and nothing is drawn.
* the three present the ladder returns `null` and nothing is drawn.
*
* Presence is `!== undefined` — the ruling's wording ("at least one of `data`,
* `staticData`, `objectName` is present"), NOT the renderers' truthiness: an
Expand Down Expand Up @@ -1836,7 +1837,8 @@ export const ObjectTreeSchema = BaseSchema.extend({
* ObjectGantt Schema
*
* `objectName` is OPTIONAL and the member ends in `requireRecordSource`
* (objectui#6939): `getDataConfig` (`plugin-gantt/src/ObjectGantt.tsx`) reads
* (objectui#6939): the shared record-source ladder `resolveRecordSourceConfig`
* (`@object-ui/core`, called by `plugin-gantt/src/ObjectGantt.tsx`) reads
* `data`, then `staticData`, then `objectName`, so a gantt authored on inline
* rows never reads the object name — three catalog entries drew correctly and
* were refused here. `data` is declared for the first time in the same stroke:
Expand All @@ -1848,8 +1850,8 @@ export const ObjectTreeSchema = BaseSchema.extend({
*/
export const ObjectGanttSchema = BaseSchema.extend({
type: z.literal('object-gantt'),
objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source getDataConfig resolves, after data and staticData; one of the three must be present (objectui#6939)'),
data: ViewDataSchema.optional().describe('Data source configuration — read FIRST by getDataConfig; undeclared on either face until objectui#6939'),
objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source resolveRecordSourceConfig resolves, after data and staticData; one of the three must be present (objectui#6939)'),
data: ViewDataSchema.optional().describe('Data source configuration — read FIRST by resolveRecordSourceConfig; undeclared on either face until objectui#6939'),
startDateField: z.string().optional().describe('Start date field'),
endDateField: z.string().optional().describe('End date field'),
titleField: z.string().optional().describe('Title field'),
Expand Down Expand Up @@ -1968,7 +1970,7 @@ export const ObjectGanttSchema = BaseSchema.extend({
// The query/data keys the fetch path reads. They were declared on
// `ObjectGridSchema` — what `ObjectGanttProps.schema` used to be typed as before
// objectui#5903 retyped it to `ObjectGanttSchema` — so they need declaring here.
staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by getDataConfig'),
staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by resolveRecordSourceConfig'),
filter: z.array(z.any()).optional().describe('Query filter, forwarded verbatim as $filter'),
sort: z.array(SortConfigSchema).optional().describe('Sort configuration, forwarded as $orderby (array only; the legacy string clause is retired — objectui#8221)'),
// objectui#10250 — the full-text pair the record query carries, declared in
Expand Down Expand Up @@ -1997,7 +1999,7 @@ export const ObjectGanttSchema = BaseSchema.extend({
*/
export const ObjectCalendarSchema = BaseSchema.extend({
type: z.literal('object-calendar'),
objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source getDataConfig resolves, after data and staticData; one of the three must be present (objectui#7313)'),
objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source resolveRecordSourceConfig resolves, after data and staticData; one of the three must be present (objectui#7313)'),
// objectui#9239 — the ARRAY arm, mirroring `ComponentPropsMap['object-calendar'].data`
// on `@objectstack/spec` (`z.array(z.unknown()).optional()`, "Pre-fetched
// records — skips the internal fetch"). ⛔ NOT `ViewDataSchema`: this member
Expand All @@ -2017,7 +2019,7 @@ export const ObjectCalendarSchema = BaseSchema.extend({
// TYPE: the TS face derives `SpecObjectCalendarProps['data']`, whose input is
// `unknown[]`, which is exactly what `z.array(z.unknown())` infers here.
data: z.array(z.unknown()).optional().describe('Pre-fetched records — an ARRAY, drawn in place of the calendar\'s own query; read FIRST by the record-source ladder. Mirrors ComponentPropsMap[\'object-calendar\'].data — the { provider, items } config object is refused by kind on this block (objectui#9239, ruling objectui#8348)'),
staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by getDataConfig'),
staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by resolveRecordSourceConfig'),
// objectui#8651 — the configuration container the SPEC declares for this
// element (`ComponentPropsMap['object-calendar'].calendar`), which this
// package's registration `inputs` already publishes and which
Expand Down
Loading