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
31 changes: 31 additions & 0 deletions .changeset/12063-unified-view-config-chart-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@object-ui/types': minor
---

feat(types)!: `UnifiedViewConfig.chart` retires with a tombstone that names the spec's list chart block, and the three `object-chart` legacy-axis refusals stop calling those keys a list-view spelling (objectui#12063)

Clause-②: no

**Retired (breaking), `@object-ui/types`.** `UnifiedViewConfig.chart` declared the list-view chart's
pre-ADR-0021 inline query: `chartType` beside `xAxisField`, `yAxisFields`, `aggregation`, `series`,
`config` and `filter`. No route reads those axes since objectui#6152 round 15, and every list-view door
refuses them by name. The member is now `chart?: never`, so a value typed `UnifiedViewConfig` that
writes `chart` stops compiling on upgrade, at any value. A plain deletion would not have refused it:
the interface keeps its `[key: string]: any` index signature, which would admit `chart` as `any`.

| before | after | write instead |
| :--- | :--- | :--- |
| `chart: { chartType, xAxisField, yAxisFields, aggregation, series, config, filter }` on a `UnifiedViewConfig` | a compile error at `chart` | the spec's list chart block, `ListChartConfig` from `@objectstack/spec/ui` (the `chart` member of its `ListView`): `{ chartType, dataset, dimensions, values }`, bound to an ADR-0021 dataset by name |

**Not measured: consumers outside the checkouts a seat can reach.** A census before this change found
nothing that imports `UnifiedViewConfig` in this repository (packages, apps, tests, docs, examples,
skills), in the ObjectStack framework checkout or in the installed dependency tree, each with a
positive control. It is a one-time reading, recorded on the pull request, and nothing re-derives it.
Host applications built on the published package were not measured.

**Changed, `@object-ui/types`.** The `object-chart` node's refusals of `xAxisField`, `yAxisFields` and
`aggregation`, and the docblocks of the same three `ObjectChartSchema` members in the published
`.d.ts`, no longer call each key "the list-view chart block's spelling": no list-view route reads that
spelling, and the list view's chart block refuses it. Each refusal and each docblock still names its
key and the same remedy: `xAxis: { field }`, `yAxis: [{ field }]` and
`aggregate: { field, function, groupBy }`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Retirement pin — `UnifiedViewConfig.chart` is RETIRED (objectui#12063,
* ADR-0049 enforce-or-remove).
*
* ## What retired, and why
*
* The member declared the list-view chart's pre-ADR-0021 inline query:
* `chartType` beside `xAxisField`, `yAxisFields`, `aggregation`, `series`,
* `config` and `filter`. objectui#6152 round 15 retired every route that read
* those axes, every list-view door refuses them by name, and nothing read or
* wrote the member. The triage ruling on objectui#12063: with zero consumers it
* retires with a tombstone that names the spec's list chart block (`dataset`,
* `values`, `dimensions`, `chartType`) as the remedy. The census is recorded on
* the card's pull request as a one-time reading; this file keeps the contract.
*
* ## Why a tombstone and not a deletion
*
* `UnifiedViewConfig` keeps a `[key: string]: any` index signature, so a
* DELETED member would read `any` and admit every value, a fresh literal
* included. Row (b) pins that consequence on an undeclared key, so the reason
* is a reading and not prose. The interface has no zod mirror, so the
* TypeScript face is the only face to retire.
*
* ## Two instruments, and which half each one reads
*
* The `Expect` / `Equal` aliases and the `@ts-expect-error` directives are
* TYPE-level: `tsc -p tsconfig.test.json` (the third leg of this package's
* `type-check` script) reads them, and a revived or re-widened member fails
* there, on an unused directive or on a `false` row. vitest strips types and
* reads none of it. The `ListChartConfigSchema` rows are RUNTIME and vitest
* reads them: they keep the remedy the tombstone names a live, parsing block.
* A green run of either one alone says nothing about the other.
*/

import { describe, expect, it } from 'vitest';
import { ListChartConfigSchema } from '@objectstack/spec/ui';
import type { ListChartConfig, ListView } from '@objectstack/spec/ui';
import type { UnifiedViewConfig } from '../designer';

/** Invariant equality — `extends` both ways would accept a narrowing. */
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

/* ── (a) the member reads as a tombstone ──────────────────────────────────── */

/**
* `?: never` without `exactOptionalPropertyTypes` is `never | undefined`, which
* collapses to `undefined`. `Equal` separates that from the object type the
* member carried and from the `any` a deletion would leave (row b).
*/
export type assertionChartReadsAsTombstone = Expect<Equal<UnifiedViewConfig['chart'], undefined>>;
/** The helper can FAIL — a sibling block is still declared, so it is not `undefined`. */
export type assertionSiblingStillDeclared = Expect<Equal<Equal<UnifiedViewConfig['kanban'], undefined>, false>>;

/* ── (b) why a tombstone: an undeclared key reads `any` here ──────────────── */

/** What a deletion would have left: the index signature answers `any` for any key it does not declare. */
export type assertionUndeclaredKeyReadsAny = Expect<Equal<UnifiedViewConfig['chartt'], any>>;

/* ── (c) the remedy is the spec's list chart block ────────────────────────── */

/** The block the tombstone names IS the spec's list view `chart` member, not a local copy. */
export type assertionRemedyIsTheSpecListViewChart = Expect<Equal<NonNullable<ListView['chart']>, ListChartConfig>>;

const LEGACY_BLOCK = {
chartType: 'bar',
xAxisField: 'status',
yAxisFields: ['estimate'],
aggregation: 'sum',
series: [{ name: 'estimate' }],
config: { stacked: true },
filter: [['status', '=', 'open']],
} as const;

const REMEDY: ListChartConfig = {
chartType: 'bar',
dataset: 'task_stats',
dimensions: ['status'],
values: ['total_estimate'],
};

describe('objectui#12063 — `UnifiedViewConfig.chart` is RETIRED', () => {
it('(c) the TypeScript face refuses the member at every value — checked by `tsc -p tsconfig.test.json`', () => {
// Not `as const` on the rows: a readonly literal would fail each line for
// a reason of its own, and each `@ts-expect-error` would pass without
// testing the tombstone.
const refused: UnifiedViewConfig[] = [
// @ts-expect-error — retired: the pre-ADR-0021 inline axes
{ id: 'legacy', type: 'chart', chart: { chartType: 'bar', xAxisField: 'status', yAxisFields: ['estimate'], aggregation: 'sum' } },
// @ts-expect-error — retired whole: the spec's block belongs on the spec's list view, not here
{ id: 'dataset', type: 'chart', chart: { chartType: 'bar', dataset: 'task_stats', values: ['total_estimate'] } },
];
// A WIDENED value is refused too: the member is declared, so it is checked
// on every assignment, where a deletion would have handed it to the index
// signature at `any`. Mutable on purpose (not `LEGACY_BLOCK`): its type is
// assignable to the retired declaration, so this row fails on the
// tombstone and on nothing else.
const widened = {
id: 'widened',
chart: { chartType: 'bar', xAxisField: 'status', yAxisFields: ['estimate'], aggregation: 'sum' },
};
// @ts-expect-error — retired: a value that reached the annotation through a wider variable
const viaVariable: UnifiedViewConfig = widened;
expect([...refused, viaVariable]).toHaveLength(3);
});

it('CONTROL — the sibling members compile and read as before', () => {
const view: UnifiedViewConfig = {
id: 'board',
type: 'kanban',
kanban: { groupByField: 'status', titleField: 'name' },
gantt: { startDateField: 'start', endDateField: 'end' },
};
expect(view.kanban?.groupByField).toBe('status');
expect(view.chart).toBeUndefined();
});

it('(d) the remedy parses: the spec\'s list chart block, bound to a dataset by name', () => {
const r = ListChartConfigSchema.safeParse(REMEDY);
expect(r.error?.issues ?? []).toEqual([]);
expect(r.success).toBe(true);
});

it('(d) the retired block\'s axes are refused BY NAME at that same spec block', () => {
const r = ListChartConfigSchema.safeParse(LEGACY_BLOCK);
expect(r.success).toBe(false);
const issues = (r.error?.issues ?? []) as Array<{ code: string; path: PropertyKey[]; keys?: string[] }>;
const unrecognized = issues.filter((i) => i.code === 'unrecognized_keys');
expect(unrecognized.map(({ code, path }) => ({ code, path }))).toEqual([{ code: 'unrecognized_keys', path: [] }]);
expect([...(unrecognized[0]!.keys ?? [])].sort()).toEqual(
['aggregation', 'config', 'filter', 'series', 'xAxisField', 'yAxisFields'],
);
});
});
33 changes: 23 additions & 10 deletions packages/types/src/designer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,16 +853,29 @@ export interface UnifiedViewConfig {
dependenciesField?: string;
colorField?: string;
};
/** Chart-specific options */
chart?: {
chartType?: string;
xAxisField?: string;
yAxisFields?: string[];
aggregation?: string;
series?: any[];
config?: any;
filter?: any;
};
/**
* RETIRED (objectui#12063, ADR-0049) — this block declared the list-view
* chart's pre-ADR-0021 inline query: `chartType` beside `xAxisField`,
* `yAxisFields`, `aggregation`, `series`, `config` and `filter`. No route
* reads those axes since objectui#6152 round 15: every list-view door refuses
* them by name, and a chart list view binds only an ADR-0021 dataset. Nothing
* read or wrote `UnifiedViewConfig.chart` either, in this repository or in
* ObjectStack (a one-time census on objectui#12063; nothing re-derives it).
*
* **Instead:** write the spec's list chart block, `ListChartConfig` from
* `@objectstack/spec/ui` (the `chart` member of its `ListView`): `chartType`,
* `dataset`, `dimensions` and `values`, which bind the chart to an ADR-0021
* dataset by name, e.g.
* `{ chartType: 'bar', dataset: 'task_stats', dimensions: ['status'], values: ['total_estimate'] }`.
*
* A tombstone rather than a deletion: this interface keeps a
* `[key: string]: any` index signature, so a deleted member would admit
* `chart` at any value, on a fresh literal too. The interface has no zod
* mirror. Pinned in `__tests__/unified-view-config-chart-retired-12063.test.ts`.
* @deprecated Not part of this contract — write the spec's list chart block
* (`dataset`, `values`, `dimensions`, `chartType`).
*/
chart?: never;

/** Catch-all for additional properties */
[key: string]: any;
Expand Down
12 changes: 6 additions & 6 deletions packages/types/src/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5554,8 +5554,8 @@ export interface ObjectChartSchema extends BaseSchema {
*/
specType?: SpecChartType;
/**
* RETIRED (objectui#10608, ADR-0049) — the list-view chart block's spelling,
* read by no `object-chart` reader. Write the spec's {@link xAxis} object,
* RETIRED (objectui#10608, ADR-0049) — no `object-chart` reader consumes
* `xAxisField`. Write the spec's {@link xAxis} object,
* `xAxis: { field: 'status' }`; on the inline `objectName` path the category
* is `aggregate.groupBy`.
*
Expand All @@ -5567,8 +5567,8 @@ export interface ObjectChartSchema extends BaseSchema {
*/
xAxisField?: never;
/**
* RETIRED (objectui#10608, ADR-0049) — the list-view chart block's spelling,
* read by no `object-chart` reader. Write the spec's {@link yAxis} list,
* RETIRED (objectui#10608, ADR-0049) — no `object-chart` reader consumes
* `yAxisFields`. Write the spec's {@link yAxis} list,
* `yAxis: [{ field: 'amount' }]` — one entry per value axis, and each entry's
* `field` is a plotted column when no `series` is written. On the inline
* `objectName` path the measure is `aggregate.field`; a dataset-bound chart
Expand All @@ -5578,8 +5578,8 @@ export interface ObjectChartSchema extends BaseSchema {
*/
yAxisFields?: never;
/**
* RETIRED (objectui#10608, ADR-0049) — the list-view chart block's spelling,
* read by no `object-chart` reader. Write the spec's {@link aggregate},
* RETIRED (objectui#10608, ADR-0049) — no `object-chart` reader consumes
* `aggregation`. Write the spec's {@link aggregate},
* `aggregate: { field, function, groupBy }`, whose `function` is the spec's
* own vocabulary; a dataset-bound chart takes its aggregation from the
* dataset's measures.
Expand Down
12 changes: 6 additions & 6 deletions packages/types/src/zod/objectql.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3772,19 +3772,19 @@ export const ObjectChartSchema = BaseSchema.extend({
// `../objectql.ts` carries the ground; the pin is
// `../__tests__/object-chart-legacy-axis-keys-retired-10608.test.ts`.
xAxisField: retirementTombstone(
'RETIRED (objectui#10608, ADR-0049) — `xAxisField` is the list-view chart block\'s spelling and no '
+ '`object-chart` reader consumes it: a node written with it draws no category axis. Write the spec\'s '
'RETIRED (objectui#10608, ADR-0049) — no `object-chart` reader consumes `xAxisField`: '
+ 'a node written with it draws no category axis. Write the spec\'s '
+ '`xAxis: { field: \'status\' }` instead; on the inline `objectName` path the category is `aggregate.groupBy`.',
),
yAxisFields: retirementTombstone(
'RETIRED (objectui#10608, ADR-0049) — `yAxisFields` is the list-view chart block\'s spelling and no '
+ '`object-chart` reader consumes it: a node written with it plots no series. Write the spec\'s '
'RETIRED (objectui#10608, ADR-0049) — no `object-chart` reader consumes `yAxisFields`: '
+ 'a node written with it plots no series. Write the spec\'s '
+ '`yAxis: [{ field: \'amount\' }]` instead, one entry per value axis; on the inline `objectName` path the '
+ 'measure is `aggregate.field`, and a dataset-bound chart selects `values` by name.',
),
aggregation: retirementTombstone(
'RETIRED (objectui#10608, ADR-0049) — `aggregation` is the list-view chart block\'s spelling and no '
+ '`object-chart` reader consumes it: a node written with it aggregates nothing. Write the spec\'s '
'RETIRED (objectui#10608, ADR-0049) — no `object-chart` reader consumes `aggregation`: '
+ 'a node written with it aggregates nothing. Write the spec\'s '
+ '`aggregate: { field, function, groupBy }` instead, `function` one of `count`, `sum`, `avg`, `min`, '
+ '`max`; a dataset-bound chart takes its aggregation from the dataset\'s measures.',
),
Expand Down
Loading