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
23 changes: 23 additions & 0 deletions .changeset/25921-types-spec-report-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@object-ui/types': patch
---

refactor(types)!: delete nine Spec Report Bridge names nothing reads

Clause-②: no (narrowing)
Metadata-protocol: none

<!-- adr-0087: not-required (runtime-export-retired @object-ui/types#SpecReportChart, @object-ui/types#SpecReportChartInput, @object-ui/types#SpecReportChartSchema, @object-ui/types#SpecReportTypeEnum, @object-ui/types#SpecReportAggregate, @object-ui/types#QLAggregationFunction, @object-ui/types#mapAggregateToQL, @object-ui/types#JoinedSpecReport, @object-ui/types#isJoinedSpecReport) the retired operands are package re-exports of `@objectstack/spec/ui` symbols, two string unions, a helper function and a type guard that no renderer, adapter or host reads; nothing in the workspace or in hotcrm imports any of them from the package entry, they are not arms of `AnySchema`, and the spec schemas they re-exported stay where they are in `@objectstack/spec/ui`, so no stored shape, registered node type or conversion surface changes and there is nothing for `objectstack migrate meta` to rewrite -->

**BREAKING** for a host that imports any of these nine names from `@object-ui/types`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected.

They are **gone from the package, not only from the entry**: `src/spec-report.ts` no longer declares them. Two of them, `mapAggregateToQL` and `isJoinedSpecReport`, are runtime values, so the built `dist` loses them too. The rest of the Spec Report Bridge stays: `SpecReport`, `SpecReportInput`, `SpecReportTypeName`, `SpecReportDateGranularity`, `SpecReportSchema`, `LegacyReportPresentationLike`, `specReportToPresentation`, `isSpecReport` and `JoinedReportBlock`.

What leaves, and what to write instead:

- FROM `SpecReportChart` TO `ReportChartParsed` from `@objectstack/spec/ui`.
- FROM `SpecReportChartInput` TO `ReportChart` from `@objectstack/spec/ui`.
- FROM `SpecReportChartSchema` TO `ReportChartSchema` from `@objectstack/spec/ui`.
- FROM `SpecReportTypeEnum` TO `ReportType` from `@objectstack/spec/ui`.
- FROM `SpecReportAggregate`, `QLAggregationFunction`, `mapAggregateToQL` TO no replacement. A spec report is dataset-bound: its `values` name measures whose aggregation the dataset declares, so there is no per-column aggregate left to translate.
- FROM `JoinedSpecReport`, `isJoinedSpecReport` TO no replacement in this package. Test `isSpecReport(value) && value.type === 'joined'` and read `value.blocks`, each a `JoinedReportBlock`.
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ import {
ChartTypeSchema as SpecChartTypeSchema,
DashboardWidgetSchema as SpecDashboardWidgetSchema,
} from '@objectstack/spec/ui';
import type {
JoinedReportBlock as SpecJoinedReportBlock,
JoinedReportBlockParsed as SpecJoinedReportBlockParsed,
} from '@objectstack/spec/ui';
import type { JoinedReportBlock as PublishedJoinedReportBlock, JoinedSpecReport } from '../spec-report.js';
import type { JoinedReportBlock as SpecJoinedReportBlock } from '@objectstack/spec/ui';
import type { JoinedReportBlock as PublishedJoinedReportBlock } from '../spec-report.js';
import { AppContextSelectorSchema } from '../zod/app.zod.js';
import { DashboardWidgetSchema, DashboardWidgetTypeSchema, GlobalFilterSchema } from '../zod/complex.zod.js';
import { DASHBOARD_COMPONENT_WIDGET_TYPES, DASHBOARD_WIDGET_TYPE_EXTENSIONS } from '../complex.js';
Expand Down Expand Up @@ -790,10 +787,10 @@ describe('renamed local dialects do not collide with a spec export (objectui#307
* `groupingsAcross`, `filter`, `chart` — plus an index signature; the spec's
* schema refuses all five of those keys, `chart` by name since objectstack#20161.
*
* objectui#10940 REPLACED the published `JoinedReportBlock` with the spec's type,
* and `JoinedSpecReport.blocks` with the spec's own `Report.blocks`: a
* published-type change, narrowing and widening, stated member by member in its
* changeset. The divergence probes that stood here — local interface against
* objectui#10940 REPLACED the published `JoinedReportBlock` with the spec's type:
* a published-type change, narrowing and widening, stated member by member in
* its changeset. (The joined-report wrapper type it also re-derived left the
* package with objectstack#25921, read by nothing; its pins went with it.) The divergence probes that stood here — local interface against
* spec, each written to stop compiling the day its difference moved — are
* answered by that replacement and are deleted with the interface.
*
Expand All @@ -807,8 +804,7 @@ describe('renamed local dialects do not collide with a spec export (objectui#307
* probe on an erased spec is deleted, so every probe below is asked on every
* compile and an erasure, on either spec, stops compiling. `any` is refused
* as before.
* - THE DERIVATION PINS: the published block IS the spec's block, and the
* published joined report's `blocks` ARE the spec's report blocks. Re-forking
* - THE DERIVATION PIN: the published block IS the spec's block. Re-forking
* the block by hand under the spec's name is also refused by
* `check:spec-symbols`, whose ALLOW row for this symbol retired with the
* burn-down.
Expand Down Expand Up @@ -861,27 +857,6 @@ const _publishedBlockIsTheSpecBlock = true satisfies (
? SameType<DeclaredBlockKeys<PublishedJoinedReportBlock>, DeclaredBlockKeys<SpecJoinedReportBlock>>
: false
);
// …and a published joined report's `blocks` are the spec's own report blocks,
// on the PARSED tier of `SpecReport`: required, each element the spec's parsed
// block, assignable to the authoring-tier block but not the other way round
// (`type` and `order[].direction` are defaulted on the parsed side).
type PublishedJoinedBlocks = BlockMember<JoinedSpecReport, 'blocks'>;
const _joinedReportBlocksAreTheSpecReportBlocks = true satisfies (
[PublishedJoinedBlocks] extends [never]
? false
: undefined extends PublishedJoinedBlocks
? false
: SameType<PublishedJoinedBlocks, SpecJoinedReportBlockParsed[]>
);
const _joinedReportBlocksAreParsedTier = true satisfies (
[PublishedJoinedBlocks] extends [never]
? false
: [SpecJoinedReportBlockParsed] extends [PublishedJoinedReportBlock]
? [PublishedJoinedReportBlock] extends [SpecJoinedReportBlockParsed]
? false
: true
: false
);

// 1. NARROWED: the legacy inline-query keys are gone from the published block,
// and so is the index signature that let any other key through.
Expand Down Expand Up @@ -958,8 +933,6 @@ void _specJoinedReportBlockTyping;
void _specJoinedReportBlockIsNotEvenAny;
void _publishedBlockTyping;
void _publishedBlockIsTheSpecBlock;
void _joinedReportBlocksAreTheSpecReportBlocks;
void _joinedReportBlocksAreParsedTier;
void _legacyBlockKeysAreGone;
void _publishedBlockHasNoIndexSignature;
void _datasetBoundBlockKeysArrived;
Expand Down
16 changes: 0 additions & 16 deletions ui/packages/types/src/__tests__/spec-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,19 @@
*
* Tests for the Spec Report bridge (spec 9.0, dataset-bound — ADR-0021):
* - Re-exports compile and match the spec shape.
* - Aggregate mapping handles the `unique → count_distinct` translation.
* - The presentation adapter maps the dataset `rows` / `values` names through.
*/

import { describe, it, expect } from 'vitest';
import {
SpecReport,
SpecReportSchema,
mapAggregateToQL,
specReportToPresentation,
isSpecReport,
} from '../spec-report';
import type { SpecReportInput } from '../spec-report';

describe('SpecReport bridge', () => {
describe('mapAggregateToQL', () => {
it('translates unique to count_distinct', () => {
expect(mapAggregateToQL('unique')).toBe('count_distinct');
});

it('passes through standard aggregates', () => {
expect(mapAggregateToQL('sum')).toBe('sum');
expect(mapAggregateToQL('avg')).toBe('avg');
expect(mapAggregateToQL('min')).toBe('min');
expect(mapAggregateToQL('max')).toBe('max');
expect(mapAggregateToQL('count')).toBe('count');
});
});

describe('SpecReportSchema parsing', () => {
it('accepts a minimal tabular report (dataset + values)', () => {
const input: SpecReportInput = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ describe('spec/ui …Schema re-exports (#2561, decision (a))', () => {
expect(typeof Types.defineStack).toBe('function');
expect(Types.ObjectStackSchema).toBeDefined();
expect(Types.SpecReportSchema).toBeDefined();
expect(Types.SpecReportTypeEnum).toBeDefined();
expect(Types.ACTION_LOCATIONS).toBeDefined();
expect(Types.ActionLocationSchema).toBeDefined();
});
Expand Down
17 changes: 8 additions & 9 deletions ui/packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,29 +960,28 @@ export type {
// Authoritative report protocol from @objectstack/spec, re-exported under
// `Spec*` names so they coexist with the legacy presentation `ReportComponentSchema`.
// See `./spec-report.ts` for the layering rationale.
//
// #25921 (batch 10): nine names of this block are DELETED, nothing in this
// workspace or in hotcrm read them: `SpecReportChart`, `SpecReportChartInput`,
// `SpecReportChartSchema` and `SpecReportTypeEnum` (import `ReportChartParsed`,
// `ReportChart`, `ReportChartSchema` and `ReportType` from `@objectstack/spec/ui`),
// and `SpecReportAggregate`, `QLAggregationFunction`, `mapAggregateToQL`,
// `JoinedSpecReport` and `isJoinedSpecReport` (no replacement). The changeset
// carries the FROM/TO.
// ---------------------------------------------------------------------------
export type {
SpecReportInput,
SpecReportChart,
SpecReportChartInput,
SpecReportTypeName,
SpecReportAggregate,
SpecReportDateGranularity,
QLAggregationFunction,
LegacyReportPresentationLike,
JoinedReportBlock,
JoinedSpecReport,
} from './spec-report.js';

export {
SpecReportSchema,
SpecReportChartSchema,
SpecReportTypeEnum,
SpecReport,
mapAggregateToQL,
specReportToPresentation,
isSpecReport,
isJoinedSpecReport,
} from './spec-report.js';

// Workflow / Flow Designer schemas removed in 9.0 — they backed the retired
Expand Down
101 changes: 5 additions & 96 deletions ui/packages/types/src/spec-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,24 @@
* without polluting the protocol.
* - A converter (`specReportToPresentation`) lets the legacy renderer keep
* working while we migrate to spec-native rendering.
*
* ## Aggregation naming
*
* The UI spec uses `aggregate: 'unique'` while the data layer (ObjectQL,
* `@objectstack/spec/data`) uses `count_distinct`. Use {@link mapAggregateToQL}
* when translating a Report column into an ObjectQL `AggregationNode`.
*/

// `@objectstack/spec` 17.0.0-rc.6 retired every `…Input` alias and moved the
// bare name onto the `z.input` side: `Report`/`ReportChart` are now
// `z.input<typeof …Schema>` and `ReportParsed`/`ReportChartParsed` carry the
// `z.infer` shape these bindings used to read under the bare name. Both pairs
// below are therefore re-pointed so that each local alias keeps the meaning it
// had at rc.5 — `SpecReport`/`SpecReportChart` stay PARSED, `SpecReportInput`/
// `SpecReportChartInput` stay INPUT. Following the rename by name alone (`X` →
// `X`) would have silently swapped the two.
// `z.infer` shape these bindings used to read under the bare name. The pair
// below is therefore re-pointed so that each local alias keeps the meaning it
// had at rc.5 — `SpecReport` stays PARSED, `SpecReportInput` stays INPUT.
// Following the rename by name alone (`X` → `X`) would have silently swapped
// the two.
import type {
ReportParsed as SpecReportType_,
Report as SpecReportInputType_,
ReportChartParsed as SpecReportChartType_,
ReportChart as SpecReportChartInputType_,
JoinedReportBlock as SpecJoinedReportBlockType_,
} from '@objectstack/spec/ui';

import {
ReportSchema as SpecReportSchema_,
ReportChartSchema as SpecReportChartSchema_,
ReportType as SpecReportTypeEnum_,
Report as SpecReportFactory_,
} from '@objectstack/spec/ui';

Expand All @@ -69,8 +59,6 @@ import {

export type SpecReport = SpecReportType_;
export type SpecReportInput = SpecReportInputType_;
export type SpecReportChart = SpecReportChartType_;
export type SpecReportChartInput = SpecReportChartInputType_;

/**
* The four report types defined by the UI Protocol.
Expand All @@ -82,15 +70,6 @@ export type SpecReportChartInput = SpecReportChartInputType_;
*/
export type SpecReportTypeName = 'tabular' | 'summary' | 'matrix' | 'joined';

/**
* Aggregation enum as it appears in the *UI* Report spec
* (`@objectstack/spec/ui` Report.columns[].aggregate).
*
* Note that this intentionally differs from the data-layer `AggregationFunction`
* enum used in ObjectQL — see {@link mapAggregateToQL}.
*/
export type SpecReportAggregate = 'sum' | 'avg' | 'max' | 'min' | 'count' | 'unique';

/**
* Date granularity for time-based grouping
* (`Report.groupingsDown[].dateGranularity`).
Expand All @@ -102,61 +81,13 @@ export type SpecReportDateGranularity = 'day' | 'week' | 'month' | 'quarter' | '
// ---------------------------------------------------------------------------

export const SpecReportSchema = SpecReportSchema_;
export const SpecReportChartSchema = SpecReportChartSchema_;
export const SpecReportTypeEnum = SpecReportTypeEnum_;

/**
* Spec factory helper (`Report.create`).
* Validates the input against the spec and returns a typed `SpecReport`.
*/
export const SpecReport = SpecReportFactory_;

// ---------------------------------------------------------------------------
// Aggregate mapping: UI spec → data layer (ObjectQL)
// ---------------------------------------------------------------------------

/**
* The aggregation enum used by the ObjectQL data layer.
* Kept as a string union (rather than importing the value from
* `@objectstack/spec/data`) so this types-only package stays
* runtime-dependency-free.
*/
export type QLAggregationFunction =
| 'count'
| 'sum'
| 'avg'
| 'min'
| 'max'
| 'count_distinct'
| 'array_agg'
| 'string_agg';

/**
* Translate a UI-layer aggregate name into the corresponding ObjectQL
* `AggregationFunction`. The only non-trivial mapping is `unique → count_distinct`.
*
* @example
* mapAggregateToQL('unique') // 'count_distinct'
* mapAggregateToQL('sum') // 'sum'
*/
export function mapAggregateToQL(aggregate: SpecReportAggregate): QLAggregationFunction {
switch (aggregate) {
case 'unique': return 'count_distinct';
case 'sum':
case 'avg':
case 'min':
case 'max':
case 'count':
return aggregate;
default: {
// Exhaustiveness check
const _exhaustive: never = aggregate;
void _exhaustive;
return 'count';
}
}
}

// ---------------------------------------------------------------------------
// Adapter: spec Report → legacy presentation ReportComponentSchema
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -282,25 +213,3 @@ export function isSpecReport(value: unknown): value is SpecReport {
* rather than kept beside it.
*/
export type JoinedReportBlock = SpecJoinedReportBlockType_;

/**
* A `SpecReport` of `type: 'joined'` carrying its constituent blocks.
* Use this as the schema input to `ReportRenderer` (dataset-bound blocks render
* through `DatasetReportRenderer`).
*
* `blocks` is the spec's own `Report.blocks`, made required. It is on the
* PARSED tier, like `SpecReport` itself (`type` and each `order[].direction`
* are defaulted), so each element is assignable to the authoring-tier
* {@link JoinedReportBlock}, but not the other way round.
*/
export type JoinedSpecReport = SpecReport & {
type: 'joined';
blocks: NonNullable<SpecReport['blocks']>;
};

/** Type guard for joined reports with a `blocks` array. */
export function isJoinedSpecReport(value: unknown): value is JoinedSpecReport {
if (!isSpecReport(value)) return false;
const v = value as Record<string, unknown>;
return v.type === 'joined' && Array.isArray((v as { blocks?: unknown }).blocks);
}
Loading