From 3574bbf71d1410a62c58e0ae5e40c3bbe4701204 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 14:23:43 +0000 Subject: [PATCH 1/4] feat(types): a TypeScript node type for each authored properties-bag carrier outside the public blocks, by reference, joined into AuthoringNode (objectui#11468) The eight ObjectQLPublicBlockComponentSchema arms and FlexBlockSchema had a zod arm and no TypeScript node type. Each is now its arm's own authoring input, derived as PublicBlockNode is: ObjectQLPublicBlockNode (one member per arm), the eight ObjectXBlockNode aliases picked from it, and FlexBlockNode. All of them join AuthoringNode, so SchemaRenderer's schema prop accepts them. The derivation now reads a bag's shape the way it reads the arm's. flex and object-chart have no spec row, so their bags are the flat mirrors' own .passthrough() members; reading the arm's shape alone left each bag open. A spec-row bag is already closed, and the 27 public blocks are typed exactly as before (pinned). Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- ...authoring-nodes-bag-carriers-11468.test.ts | 183 ++++++++++++++++++ packages/types/src/authoring-nodes.ts | 106 +++++++++- packages/types/src/index.ts | 12 ++ 3 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts diff --git a/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts b/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts new file mode 100644 index 0000000000..6ff92fe483 --- /dev/null +++ b/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts @@ -0,0 +1,183 @@ +/** + * 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. + */ + +/** + * objectui#11468: the authored `properties`-bag carriers outside the public + * blocks (the eight `ObjectQLPublicBlockComponentSchema` arms and + * `FlexBlockSchema`) have a TypeScript node type, derived by reference and + * joined into `AuthoringNode`, pinned. + * + * As in `authoring-nodes-11364.test.ts`, most assertions here are TYPE-level, + * judged by this package's `type-check` (its `tsc -p tsconfig.test.json` leg), + * never by vitest. `PostRemovalSchema` is `SchemaRenderer`'s `schema` prop with + * `BaseSchema`'s index signature stripped at the type level, so the pins run + * now, while the signature is still present, and keep running after + * objectui#8347 removes it. + */ + +import { describe, expect, it } from 'vitest'; +import type { z } from 'zod'; +import type { BaseSchema } from '../base.js'; +import type { + AuthoringNode, + FlexBlockNode, + ObjectChartBlockNode, + ObjectFormBlockNode, + ObjectGanttBlockNode, + ObjectGridBlockNode, + ObjectMapBlockNode, + ObjectMasterDetailFormBlockNode, + ObjectMetricBlockNode, + ObjectQLPublicBlockNode, + ObjectTimelineBlockNode, + PublicBlockNode, +} from '../authoring-nodes.js'; +import { + FlexBlockSchema, + ObjectQLPublicBlockComponentSchema, + PublicBlockComponentSchema, +} from '../zod/index.zod.js'; + +type Assert = T; +type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; + +/** `T` without its string / number index signatures: `BaseSchema` as objectui#8347 leaves it. */ +type WithoutIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; +}; + +/** `SchemaRendererProps['schema']` with the removal applied to its `BaseSchema` member. */ +type PostRemovalSchema = WithoutIndexSignature | AuthoringNode | string | null | undefined; + +/** + * The `type` of every member whose `properties` bag accepts an undeclared key: + * a string index signature whose value is not `never`. (`element:divider`'s + * bag is `Record`, which refuses every key, so it is closed.) + */ +type OpenBagTypes = U extends { type: infer T; properties?: infer Bag } + ? string extends keyof NonNullable + ? [NonNullable[string]] extends [never] + ? never + : T + : never + : never; + +/** The derivation before objectui#11468: the arm's shape read closed, its bag read as it stands. */ +type ShapeOnlyArmInput = Arm extends z.ZodObject ? z.input> : never; + +// 1. No arm is lost. The derivation maps anything that is not a zod object to +// `never`, so the derived `type` sets must equal the arms' own. +type _EveryObjectQLArmIsTyped = Assert< + Equal['type']> +>; +type _FlexIsTyped = Assert['type']>>; + +// 2. Each named node type is its arm's member, and every one is in `AuthoringNode`. +type _EachAliasIsItsArm = Assert< + Equal< + [ + ObjectMetricBlockNode['type'], + ObjectMasterDetailFormBlockNode['type'], + ObjectTimelineBlockNode['type'], + ObjectFormBlockNode['type'], + ObjectMapBlockNode['type'], + ObjectChartBlockNode['type'], + ObjectGanttBlockNode['type'], + ObjectGridBlockNode['type'], + ], + [ + 'object-metric', + 'object-master-detail-form', + 'object-timeline', + 'object-form', + 'object-map', + 'object-chart', + 'object-gantt', + 'object-grid', + ] + > +>; +type _JoinedIntoAuthoringNode = Assert< + Equal, ObjectQLPublicBlockNode | FlexBlockNode> +>; + +// 3. No member of `AuthoringNode` carries an open bag. `flex` and `object-chart` +// have no spec row: their bags are the flat mirrors' own members, kept +// `.passthrough()` for the tolerant face, so reading the arm's shape alone +// leaves each bag with `[k: string]: unknown`. The control shows that, which +// is why the derivation reads the bag's shape too. +type _NoAuthoringNodeBagIsOpen = Assert, never>>; +type _ControlShapeOnlyLeavesTwoBagsOpen = Assert< + Equal< + OpenBagTypes>, + 'object-chart' | 'flex' + > +>; + +// 4. Closing the bag changes nothing for a spec-row bag: the 27 public blocks +// are typed exactly as the shape-only derivation typed them. +type _PublicBlocksUnchanged = Assert< + Equal> +>; + +// 5. Every new node type is accepted where `SchemaRenderer` takes a schema, after the removal too. +type _EveryCarrierFitsThePostRemovalProp = Assert< + ObjectQLPublicBlockNode | FlexBlockNode extends PostRemovalSchema ? true : false +>; + +describe('objectui#11468: the authored properties-bag carriers have a TypeScript authoring face', () => { + it('object-map: the spec row is the bag, a misspelled bag key and a flat prop are refused, after the removal', () => { + const spelled: PostRemovalSchema = { + type: 'object-map', + properties: { objectName: 'stores', map: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' } }, + }; + // @ts-expect-error `objectNme` is not a member of ComponentPropsMap['object-map'] + const misspelled: PostRemovalSchema = { type: 'object-map', properties: { objectNme: 'stores' } }; + // @ts-expect-error `objectName` is a member of the bag, refused flat on the node by name + const flat: ObjectMapBlockNode = { type: 'object-map', objectName: 'stores' }; + expect([spelled, misspelled, flat]).toHaveLength(3); + }); + + it('flex: the bag is closed, and the child list lives in it', () => { + const box: FlexBlockNode = { + type: 'flex', + className: 'h-screen', + properties: { direction: 'col', gap: 4, children: [{ type: 'text', content: 'Hello' }] }, + }; + // @ts-expect-error `direciton` is not a member of the flex bag (the mirror's members, closed) + const misspelled: FlexBlockNode = { type: 'flex', properties: { direciton: 'col' } }; + // @ts-expect-error `direction` is a member of the bag, refused flat on the node by name + const flat: FlexBlockNode = { type: 'flex', direction: 'col' }; + // @ts-expect-error a node-level child list is refused: it is `properties.children` + const nodeChildren: FlexBlockNode = { type: 'flex', children: [] }; + // @ts-expect-error `body` is refused toward `properties.children` + const body: FlexBlockNode = { type: 'flex', body: [] }; + const asProp: PostRemovalSchema = box; + expect([box, misspelled, flat, nodeChildren, body, asProp]).toHaveLength(6); + }); + + it('object-chart: the bag is closed, though it is no spec row', () => { + const chart: ObjectChartBlockNode = { + type: 'object-chart', + properties: { objectName: 'opportunity', chartType: 'bar' }, + }; + // @ts-expect-error `chartTyp` is not a member of the object-chart bag (the mirror's members, closed) + const misspelled: ObjectChartBlockNode = { type: 'object-chart', properties: { chartTyp: 'bar' } }; + expect([chart, misspelled]).toHaveLength(2); + }); + + it('every carrier arm is a zod object, and so are the two mirror bags: the premises the derivation reads', () => { + const arms: z.ZodType[] = [...ObjectQLPublicBlockComponentSchema.options, FlexBlockSchema]; + expect(arms.filter((arm) => arm.def.type !== 'object')).toEqual([]); + expect(ObjectQLPublicBlockComponentSchema.options.length).toBeGreaterThan(0); + for (const bag of [FlexBlockSchema.shape.properties, ObjectQLPublicBlockComponentSchema.options.find((arm) => arm.shape.type.value === 'object-chart')?.shape.properties]) { + expect(bag?.def.type).toBe('optional'); + expect((bag?.def as { innerType: z.ZodType } | undefined)?.innerType.def.type).toBe('object'); + } + }); +}); diff --git a/packages/types/src/authoring-nodes.ts b/packages/types/src/authoring-nodes.ts index a2e6ac965e..de2c2fa752 100644 --- a/packages/types/src/authoring-nodes.ts +++ b/packages/types/src/authoring-nodes.ts @@ -20,7 +20,9 @@ * with its `properties` bag, a `page:tabs` with `properties.items`, an * `action:button` with its executor keys in the bag (objectui#11183), and a * stored page document handed to `SchemaRenderer` under its page kind. Each of - * them is declared by the spec, and the renderer draws it. + * them is declared by the spec, and the renderer draws it. objectui#11468 adds + * the other authored `properties`-bag carriers the zod face arms: the eight + * ObjectQL blocks (`object-map`, `object-grid`, …) and `flex`. * * ## How each one is derived * @@ -33,6 +35,17 @@ * config is left closed. Every member, value type and refusal is the arm's, * and through the arm the spec row's (`propsBag`); nothing is restated, and * an arm added to that union is typed here the same day. + * - {@link ObjectQLPublicBlockNode} and {@link FlexBlockNode} + * (objectui#11468): the same derivation, over the arms of + * `ObjectQLPublicBlockComponentSchema` (`./zod/objectql.zod.ts`) and over + * `FlexBlockSchema` (`./zod/layout.zod.ts`), the authored + * `properties`-bag carriers outside `PublicBlockComponentSchema`. Two of + * their bags are not spec rows: `flex` and `object-chart` have no + * `ComponentPropsMap` row, so each bag is its flat mirror's own members, + * kept `.passthrough()` for the tolerant face (objectui#11276). The + * derivation reads a bag's shape the way it reads the arm's, so the type + * is closed where the strict authoring face closes it. A spec-row bag is + * already closed, and the derivation changes nothing for it. * - {@link ElementTextInputNode} and {@link ElementRecordPickerNode}: no zod * arm exists for these two. `AnyComponentSchema` still refuses both at * `type`, and `registered-types-validate-ratchet-10859` in @@ -81,17 +94,35 @@ import type { PageSchema as SpecPageSchema, } from '@objectstack/spec/ui'; import type { BaseSchema as BaseSchemaMirror } from './zod/base.zod.js'; +import type { FlexBlockSchema } from './zod/layout.zod.js'; +import type { ObjectQLPublicBlockComponentSchema } from './zod/objectql.zod.js'; import type { NODE_ENVELOPE, PublicBlockComponentSchema } from './zod/public-blocks.zod.js'; import type { retirementTombstone } from './zod/tombstone.zod.js'; +/** + * An arm's `properties` member, read the way {@link ClosedArmInput} reads the + * arm: when the bag is a zod object, its shape with the object config left + * closed. A bag that is a spec row is already closed, so this changes nothing + * for it. A bag built from a flat `.passthrough()` mirror (`flex`'s + * `FlexPropsBag`, `object-chart`'s `ObjectChartPropsBag`, objectui#11276) keeps + * the mirror's tolerant posture, which the strict authoring face closes, and + * this type closes it the same way. Any other member is left as it is. + */ +type ClosedBag = Member extends z.ZodOptional + ? z.ZodOptional> + : Member; + /** * A zod object arm's authoring input, read off its shape with the object * config left closed: the arm's `.passthrough()` index signature does not - * reach the type. Distributes over a union of arms. Anything that is not an - * object arm maps to `never`, and the pin file checks that no arm is lost - * that way. + * reach the type, nor does its bag's ({@link ClosedBag}). Distributes over a + * union of arms. Anything that is not an object arm maps to `never`, and the + * pin file checks that no arm is lost that way. */ -type ClosedArmInput = Arm extends z.ZodObject ? z.input> : never; +type ClosedArmInput = Arm extends z.ZodObject ? z.input>> : never; + +/** An arm's shape with its `properties` member closed ({@link ClosedBag}); every other member as it is. */ +type ClosedArmShape = { [K in keyof Shape]: K extends 'properties' ? ClosedBag : Shape[K] }; /** What `.extend()` does to a shape: `Own`'s keys replace `Base`'s, the rest stay. */ type ExtendedShape = Omit & Own; @@ -142,6 +173,63 @@ export type PublicBlockNode = ClosedArmInput<(typeof PublicBlockComponentSchema) /** The {@link PublicBlockNode} member whose `type` is `T`. */ export type PublicBlockNodeOf = Extract; +/** + * The ObjectQL public blocks the zod face arms in their own union, + * `ObjectQLPublicBlockComponentSchema` (`./zod/objectql.zod.ts`, objectui#10859 + * and objectui#11276): `object-metric`, `object-master-detail-form`, + * `object-timeline`, `object-form`, `object-map`, `object-chart`, + * `object-gantt` and `object-grid`, each the arm's own authoring input + * (objectui#11468), derived exactly as {@link PublicBlockNode} is. The block's + * props are its `properties` bag: the spec's `ComponentPropsMap` row, or, for + * `object-chart`, which has no row, the `ObjectChartSchema` mirror's own + * members, closed ({@link ClosedBag}). + * + * ⚠️ Not the TypeScript twins in `./objectql.ts` (`ObjectFormSchema`, + * `ObjectMapSchema`, `ObjectGanttSchema`, `ObjectChartSchema`, + * `ObjectGridSchema`): each of those is the node as its renderer reads it after + * `SchemaRenderer` hoists the bag, and as code composes it. These are the + * authored node, its props in the bag. + */ +export type ObjectQLPublicBlockNode = ClosedArmInput<(typeof ObjectQLPublicBlockComponentSchema)['options'][number]>; + +/** The {@link ObjectQLPublicBlockNode} member whose `type` is `T`. */ +type ObjectQLPublicBlockNodeOf = Extract; + +/** An authored `object-metric` node: the `ObjectMetricBlockSchema` arm's input. */ +export type ObjectMetricBlockNode = ObjectQLPublicBlockNodeOf<'object-metric'>; + +/** An authored `object-master-detail-form` node: the `ObjectMasterDetailFormBlockSchema` arm's input. */ +export type ObjectMasterDetailFormBlockNode = ObjectQLPublicBlockNodeOf<'object-master-detail-form'>; + +/** An authored `object-timeline` node: the `ObjectTimelineBlockSchema` arm's input. */ +export type ObjectTimelineBlockNode = ObjectQLPublicBlockNodeOf<'object-timeline'>; + +/** An authored `object-form` node: the `ObjectFormBlockSchema` arm's input. */ +export type ObjectFormBlockNode = ObjectQLPublicBlockNodeOf<'object-form'>; + +/** An authored `object-map` node: the `ObjectMapBlockSchema` arm's input. */ +export type ObjectMapBlockNode = ObjectQLPublicBlockNodeOf<'object-map'>; + +/** An authored `object-chart` node: the `ObjectChartBlockSchema` arm's input. */ +export type ObjectChartBlockNode = ObjectQLPublicBlockNodeOf<'object-chart'>; + +/** An authored `object-gantt` node: the `ObjectGanttBlockSchema` arm's input. */ +export type ObjectGanttBlockNode = ObjectQLPublicBlockNodeOf<'object-gantt'>; + +/** An authored `object-grid` node: the `ObjectGridBlockSchema` arm's input. */ +export type ObjectGridBlockNode = ObjectQLPublicBlockNodeOf<'object-grid'>; + +/** + * An authored `flex` node: the `FlexBlockSchema` arm's input (objectui#11468), + * derived as {@link PublicBlockNode} is. Its props, the child list included, + * are its `properties` bag: the `FlexSchema` mirror's own members + * (`direction`, `justify`, `align`, `gap`, `wrap`, `children`), closed + * ({@link ClosedBag}), because `@objectstack/spec` has no `flex` row + * (objectui#11276). The TypeScript `FlexSchema` (`./layout.ts`) stays the node + * as the `flex` renderer reads it after the hoist. + */ +export type FlexBlockNode = ClosedArmInput; + /** * `element:text_input`: `ComponentPropsMap['element:text_input']` * (`ElementTextInputPropsSchema`) as its `properties` bag, by reference. @@ -177,4 +265,10 @@ export type PageDocumentNode = SpecPageInput & { type: NonNullable Date: Fri, 2 Oct 2026 14:28:31 +0000 Subject: [PATCH 2/4] feat(types): the dashboard example types its authored flex nodes as FlexBlockNode; changeset (objectui#11468) The example annotated its root flex node BaseSchema and checked each bag with satisfies FlexLayoutProps, which typed the nested flex node's bag against SchemaNode. Both flex nodes are now FlexBlockNode, the arm's own input. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- .changeset/11468-bag-carrier-node-types.md | 37 ++++++++++++++++++++++ packages/types/examples/dashboard.ts | 15 +++++---- 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 .changeset/11468-bag-carrier-node-types.md diff --git a/.changeset/11468-bag-carrier-node-types.md b/.changeset/11468-bag-carrier-node-types.md new file mode 100644 index 0000000000..bed87507ac --- /dev/null +++ b/.changeset/11468-bag-carrier-node-types.md @@ -0,0 +1,37 @@ +--- +'@object-ui/types': minor +--- + +The authored `properties`-bag carriers outside the spec's public blocks have a +TypeScript authoring type, and `AuthoringNode` includes them, so +`SchemaRenderer`'s `schema` prop accepts them (objectui#11468). + +`@object-ui/types` exports, each derived by reference from its zod arm and none +restated: + +- `ObjectQLPublicBlockNode`: one member per arm of the zod face's + `ObjectQLPublicBlockComponentSchema`, each the arm's own input, derived the + way `PublicBlockNode` is. The eight members are also exported by name: + `ObjectMetricBlockNode`, `ObjectMasterDetailFormBlockNode`, + `ObjectTimelineBlockNode`, `ObjectFormBlockNode`, `ObjectMapBlockNode`, + `ObjectChartBlockNode`, `ObjectGanttBlockNode` and `ObjectGridBlockNode`. + Each is the authored node, its props in the `properties` bag. The flat + `ObjectFormSchema`, `ObjectMapSchema`, `ObjectGanttSchema`, + `ObjectChartSchema` and `ObjectGridSchema` stay the node as its renderer + reads it after the hoist. +- `FlexBlockNode`: the `FlexBlockSchema` arm's input, its props and its child + list in the `properties` bag. `FlexSchema` stays the node as the `flex` + renderer reads it. + +The derivation now reads a bag's shape the way it reads the arm's. `flex` and +`object-chart` have no `ComponentPropsMap` row, so each bag is its flat +mirror's own members, kept open for the tolerant zod face; on these node types +the bag is closed, as the strict authoring face closes it, so a misspelled bag +key does not compile. A spec-row bag is already closed: `PublicBlockNode` is +unchanged, which `authoring-nodes-bag-carriers-11468.test.ts` pins. + +Adding members to `AuthoringNode` widens what the prop accepts; every value it +accepted before is still accepted. Reading is a different matter: on a union, a +key that one closed member does not declare is TS2339, so code that reads a key +off `SchemaRendererProps['schema']` or off `toRenderableSchema`'s return needs +to narrow first. diff --git a/packages/types/examples/dashboard.ts b/packages/types/examples/dashboard.ts index 8de20790c2..0d9ef939f4 100644 --- a/packages/types/examples/dashboard.ts +++ b/packages/types/examples/dashboard.ts @@ -13,11 +13,14 @@ * sidebar, header, and data table. */ -import type { BaseSchema, FlexLayoutProps, SidebarSchema, HeaderBarSchema, CardSchema, DataTableSchema } from '../src/index'; +import type { FlexBlockNode, SidebarSchema, HeaderBarSchema, CardSchema, DataTableSchema } from '../src/index'; // An authored `flex` node takes its props, the child list included, in its -// `properties` bag (objectui#11276); `satisfies FlexLayoutProps` checks each bag. -export const dashboardSchema: BaseSchema = { +// `properties` bag (objectui#11276). `FlexBlockNode` is that node, its bag +// closed (objectui#11468). The bag's child list is `unknown[]`, as the zod arm +// declares it (the page walk judges each entry), so the nested `flex` node is +// checked with `satisfies FlexBlockNode`. +export const dashboardSchema: FlexBlockNode = { type: 'flex', className: 'h-screen', properties: { @@ -92,8 +95,8 @@ export const dashboardSchema: BaseSchema = { ] } ] - } satisfies FlexLayoutProps - } + } + } satisfies FlexBlockNode ] - } satisfies FlexLayoutProps + } }; From 4452f3ae5534fed21f3d3b535c310232498b1008 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 14:42:57 +0000 Subject: [PATCH 3/4] docs: the authored properties-bag carriers are annotated with their node types, not BaseSchema (objectui#11468) Each doc block that authors an object-grid, object-form, object-map, object-gantt or flex node in its properties bag now annotates the arm's node type (ObjectGridBlockNode, ObjectFormBlockNode, ObjectMapBlockNode, ObjectGanttBlockNode, FlexBlockNode). The bag is checked against the arm, and the block keeps compiling once BaseSchema's index signature goes. The types README lists the new node types; its nested flex example names each inline child's type with satisfies, because the flex bag's child list is unknown[] as the arm declares it. Five plugin-map.mdx blocks keep BaseSchema: each also writes onMarkerClick on the node, a key the object-map arm does not declare and its node type refuses. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- README.md | 4 +- content/docs/guide/objectos-integration.mdx | 4 +- content/docs/plugins/plugin-gantt.mdx | 40 +++++++++---------- content/docs/plugins/plugin-map.mdx | 28 ++++++------- content/docs/utilities/data-objectstack.mdx | 4 +- content/docs/utilities/index.md | 4 +- packages/plugin-form/README.md | 8 ++-- packages/plugin-gantt/README.md | 4 +- packages/plugin-grid/README.md | 44 ++++++++++----------- packages/plugin-map/README.md | 8 ++-- packages/types/README.md | 26 ++++++++---- 11 files changed, 93 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 4175f4340a..0c6d469f04 100644 --- a/README.md +++ b/README.md @@ -327,7 +327,7 @@ declare class MyCustomDataSource implements DataSource { **Stop writing repetitive UI code.** A form is a schema, not a component: ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectFormBlockNode } from '@object-ui/types'; // Traditional React: useState, validation, handlers, JSX — per form function UserForm() { @@ -336,7 +336,7 @@ function UserForm() { // Object UI: declare it — the block's props go in its `properties` bag, // as `@objectstack/spec` declares them -const schema: BaseSchema = { +const schema: ObjectFormBlockNode = { type: "object-form", properties: { objectName: "user", diff --git a/content/docs/guide/objectos-integration.mdx b/content/docs/guide/objectos-integration.mdx index de0fbfe534..169b5c7e36 100644 --- a/content/docs/guide/objectos-integration.mdx +++ b/content/docs/guide/objectos-integration.mdx @@ -259,7 +259,7 @@ const sysUserView: ObjectViewSchema = { ```typescript import { ObjectStackAdapter } from '@object-ui/data-objectstack'; -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; const dataSource = new ObjectStackAdapter({ @@ -272,7 +272,7 @@ const dataSource = new ObjectStackAdapter({ // (`{ object, view?, filter?, sort?, limit? }`), not the adapter, and // `SchemaRenderer` strips it from the props it spreads. The grid's own props go // in its `properties` bag — the spec's `ComponentPropsMap['object-grid']` row. -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'contact', diff --git a/content/docs/plugins/plugin-gantt.mdx b/content/docs/plugins/plugin-gantt.mdx index 256ab3d6f9..eac58762c7 100644 --- a/content/docs/plugins/plugin-gantt.mdx +++ b/content/docs/plugins/plugin-gantt.mdx @@ -71,9 +71,9 @@ renderer reads it, and as code composes it. ```tsx import '@object-ui/plugin-gantt' -import type { BaseSchema } from '@object-ui/types' +import type { ObjectGanttBlockNode } from '@object-ui/types' -const schema: BaseSchema = { +const schema: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'tasks', // Your ObjectQL object @@ -91,9 +91,9 @@ const schema: BaseSchema = { ### With Static Data ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const schema: BaseSchema = { +const schema: ObjectGanttBlockNode = { type: 'object-gantt', properties: { staticData: [ @@ -215,9 +215,9 @@ with a time part is an instant, and the bar ends exactly there. The timeline's Map your database fields to Gantt properties: ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const fieldMappedGantt: BaseSchema = { +const fieldMappedGantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'project_tasks', @@ -283,9 +283,9 @@ The dependencies field should contain an array of task IDs that this task depend ### Object Provider (Database) ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const objectProviderGantt: BaseSchema = { +const objectProviderGantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'project_tasks', @@ -302,9 +302,9 @@ const objectProviderGantt: BaseSchema = { ### Value Provider (Static) ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const valueProviderGantt: BaseSchema = { +const valueProviderGantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { staticData: [ @@ -323,9 +323,9 @@ const valueProviderGantt: BaseSchema = { ### API Provider ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const apiProviderGantt: BaseSchema = { +const apiProviderGantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { data: { @@ -422,9 +422,9 @@ no object (no `objectName`, no object `data` provider), no mode opens anything. ### Software Project ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const softwareProject: BaseSchema = { +const softwareProject: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'sprint_tasks', @@ -442,9 +442,9 @@ const softwareProject: BaseSchema = { ### Construction Project ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const constructionGantt: BaseSchema = { +const constructionGantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'construction_phases', @@ -461,9 +461,9 @@ const constructionGantt: BaseSchema = { ### Marketing Campaign ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const campaignGantt: BaseSchema = { +const campaignGantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { staticData: [ @@ -544,7 +544,7 @@ const campaignGantt: BaseSchema = { ## TypeScript Support ```ts -import type { BaseSchema, GanttConfig } from '@object-ui/types' +import type { ObjectGanttBlockNode, GanttConfig } from '@object-ui/types' const ganttConfig: GanttConfig = { startDateField: 'startDate', @@ -554,7 +554,7 @@ const ganttConfig: GanttConfig = { dependenciesField: 'dependencies' } -const ganttSchema: BaseSchema = { +const ganttSchema: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'project_tasks', diff --git a/content/docs/plugins/plugin-map.mdx b/content/docs/plugins/plugin-map.mdx index 262ae1dbea..f2202e9e20 100644 --- a/content/docs/plugins/plugin-map.mdx +++ b/content/docs/plugins/plugin-map.mdx @@ -61,9 +61,9 @@ the renderer reads it, and as code composes it. ```tsx import '@object-ui/plugin-map' -import type { BaseSchema } from '@object-ui/types' +import type { ObjectMapBlockNode } from '@object-ui/types' -const schema: BaseSchema = { +const schema: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'locations', // Your ObjectQL object @@ -80,9 +80,9 @@ const schema: BaseSchema = { ### With Static Data ```tsx -import type { BaseSchema } from '@object-ui/types' +import type { ObjectMapBlockNode } from '@object-ui/types' -const schema: BaseSchema = { +const schema: ObjectMapBlockNode = { type: 'object-map', properties: { staticData: [ @@ -198,9 +198,9 @@ the way in. Both are gone. Declare the binding: When your data has separate latitude and longitude fields: ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const storeMap: BaseSchema = { +const storeMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'stores', @@ -219,9 +219,9 @@ const storeMap: BaseSchema = { When your data has a combined location field: ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const placeMap: BaseSchema = { +const placeMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'places', @@ -254,9 +254,9 @@ Two cases sit outside the fit: Declare either one to take the camera over and opt this view out of the auto-fit: ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const cameraMap: BaseSchema = { +const cameraMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'locations', @@ -279,9 +279,9 @@ the centre of the records, `center` on its own at a continental zoom. ### Object Provider (Database) ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const retailStores: BaseSchema = { +const retailStores: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'retail_stores', @@ -609,7 +609,7 @@ code hands it to the component directly (``); an authored node writes its props in the bag. ```ts -import type { BaseSchema, ObjectMapSchema, ObjectMapConfig } from '@object-ui/types' +import type { ObjectMapBlockNode, ObjectMapSchema, ObjectMapConfig } from '@object-ui/types' const mapConfig: ObjectMapConfig = { latitudeField: 'lat', @@ -621,7 +621,7 @@ const mapConfig: ObjectMapConfig = { } // The authored node: its props in the `properties` bag. -const mapNode: BaseSchema = { +const mapNode: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'locations', diff --git a/content/docs/utilities/data-objectstack.mdx b/content/docs/utilities/data-objectstack.mdx index 2e3058e761..b60e7d2a84 100644 --- a/content/docs/utilities/data-objectstack.mdx +++ b/content/docs/utilities/data-objectstack.mdx @@ -64,14 +64,14 @@ React wiring lives in `@object-ui/react`, not here. Wrap your tree in ```tsx import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; import { createObjectStackAdapter } from '@object-ui/data-objectstack'; -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com', }); // The block's props go in its `properties` bag, as `@objectstack/spec` declares them. -const mySchema: BaseSchema = { type: 'object-grid', properties: { objectName: 'user' } }; +const mySchema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'user' } }; function App() { return ( diff --git a/content/docs/utilities/index.md b/content/docs/utilities/index.md index 43ef880d17..ac6a3c1fce 100644 --- a/content/docs/utilities/index.md +++ b/content/docs/utilities/index.md @@ -165,7 +165,7 @@ through `@object-ui/react`'s `SchemaRendererProvider`: ```tsx import { createObjectStackAdapter } from '@object-ui/data-objectstack'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com', @@ -173,7 +173,7 @@ const dataSource = createObjectStackAdapter({ }); // The block's props go in its `properties` bag, as `@objectstack/spec` declares them. -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'account', diff --git a/packages/plugin-form/README.md b/packages/plugin-form/README.md index bb9027269a..93bd6b493e 100644 --- a/packages/plugin-form/README.md +++ b/packages/plugin-form/README.md @@ -722,11 +722,11 @@ steps are its `sections` — one step per section. There is no unknown-component placeholder and the fields inside `steps` are never read. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectFormBlockNode } from '@object-ui/types'; // The props go in the node's `properties` bag — the members of // `@objectstack/spec`'s `ComponentPropsMap['object-form']` row. -const schema: BaseSchema = { +const schema: ObjectFormBlockNode = { type: 'object-form', properties: { objectName: 'contacts', // the object whose fields the steps list @@ -972,14 +972,14 @@ context, which `ObjectFormRenderer` reads at `src/index.tsx` before handing import { SchemaRendererProvider, SchemaRenderer } from '@object-ui/react'; import { createObjectStackAdapter } from '@object-ui/data-objectstack'; import '@object-ui/plugin-form'; -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectFormBlockNode } from '@object-ui/types'; const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com', token: 'your-auth-token', }); -const schema: BaseSchema = { +const schema: ObjectFormBlockNode = { type: 'object-form', properties: { objectName: 'users', diff --git a/packages/plugin-gantt/README.md b/packages/plugin-gantt/README.md index 07eae5827b..73e2a8c0d7 100644 --- a/packages/plugin-gantt/README.md +++ b/packages/plugin-gantt/README.md @@ -894,9 +894,9 @@ a task array. There is no `GanttSchema`. `ObjectGanttSchema` in `@object-ui/type yourself. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGanttBlockNode } from '@object-ui/types'; -const gantt: BaseSchema = { +const gantt: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'project_tasks', diff --git a/packages/plugin-grid/README.md b/packages/plugin-grid/README.md index 3b12331a12..7d1bf5d556 100644 --- a/packages/plugin-grid/README.md +++ b/packages/plugin-grid/README.md @@ -179,10 +179,10 @@ before the grid runs, so `ObjectGridSchema` (`@object-ui/types`) is the node as grid reads it — and the type of ``'s `schema` prop. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const grid: BaseSchema = { +const grid: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -373,10 +373,10 @@ up leaking `columns="[object Object]"` into the DOM instead of drawing a table ### Basic Grid ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -403,10 +403,10 @@ fixtures, tests — give it a `ViewData` with the `value` provider. The rows go under `items`; a bare array is the deprecated `staticData` spelling. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -487,10 +487,10 @@ Not covered: ### Selectable Grid ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -511,10 +511,10 @@ see [Row callbacks are component props](#row-callbacks-are-component-props). ### Grid with Pagination ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -605,7 +605,7 @@ host installs once, above the whole tree: import { SchemaRendererProvider, SchemaRenderer } from '@object-ui/react'; import { createObjectStackAdapter } from '@object-ui/data-objectstack'; import '@object-ui/plugin-grid'; -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; const dataSource = createObjectStackAdapter({ @@ -613,7 +613,7 @@ const dataSource = createObjectStackAdapter({ token: 'your-auth-token', }); -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -658,10 +658,10 @@ Columns sort by default. `sortable` is a **per-column** key, used to turn a colu off; the grid-level `sort` declares the order the grid opens with. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -681,10 +681,10 @@ baked into the metadata, and a toolbar search over the fields named in `searchableFields`. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -705,10 +705,10 @@ themselves live in the object's action set, so the same action behaves identical wherever it is offered. They are `string[]`, not inline definitions with callbacks. ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -801,10 +801,10 @@ indistinguishable from a broken build (objectui#8674). Enable inline cell editing for quick updates: ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'users', @@ -852,10 +852,10 @@ The schema half is just `editable` — the save/cancel affordances appear on the own once a row has pending changes: ```typescript -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectGridBlockNode } from '@object-ui/types'; import type { ObjectGridProps } from '@objectstack/spec/ui'; -const schema: BaseSchema = { +const schema: ObjectGridBlockNode = { type: 'object-grid', properties: { objectName: 'products', diff --git a/packages/plugin-map/README.md b/packages/plugin-map/README.md index 4fa47ae1ab..6ed73d8e60 100644 --- a/packages/plugin-map/README.md +++ b/packages/plugin-map/README.md @@ -68,10 +68,10 @@ bag member, as the spec's own page component does (objectui#10859). ```ts import '@object-ui/plugin-map'; -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; // Object-bound: the markers are the records the query returns. -const schema: BaseSchema = { +const schema: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'stores', @@ -88,9 +88,9 @@ const schema: BaseSchema = { A literal record array instead of a query, with the same `map` block: ```ts -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const schema: BaseSchema = { +const schema: ObjectMapBlockNode = { type: 'object-map', properties: { staticData: [ diff --git a/packages/types/README.md b/packages/types/README.md index 0e5e689132..edfb68ebe7 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -131,6 +131,16 @@ restated by hand: - `PublicBlockNode`: every public block the zod face arms (`element:text`, `page:tabs`, `action:button`, ...), each the arm's own input. `PublicBlockNodeOf<'element:text'>` picks one. +- `ObjectQLPublicBlockNode`: every ObjectQL block the zod face arms in + `ObjectQLPublicBlockComponentSchema`, each the arm's own input. Each is also + exported by name: `ObjectGridBlockNode`, `ObjectFormBlockNode`, + `ObjectMapBlockNode`, `ObjectGanttBlockNode`, `ObjectChartBlockNode`, + `ObjectMetricBlockNode`, `ObjectTimelineBlockNode` and + `ObjectMasterDetailFormBlockNode`. +- `FlexBlockNode`: an authored `flex` node, its layout props and its child + list in the bag. `flex` and `object-chart` have no `ComponentPropsMap` row, + so each bag is the flat type's own members (`FlexSchema`, + `ObjectChartSchema`), closed like a row. - `ElementTextInputNode` and `ElementRecordPickerNode`: the spec rows of `element:text_input` and `element:record_picker`. - `PageDocumentNode`: a stored page document under its page kind @@ -403,7 +413,7 @@ function render(schema: AnySchema) { Components can nest indefinitely: ```typescript -import type { BaseSchema, ContainerSchema, FlexLayoutProps, SidebarSchema } from '@object-ui/types'; +import type { ContainerSchema, FlexBlockNode, HeaderBarSchema, SidebarSchema } from '@object-ui/types'; // The two leaves are annotated so the nesting below is checked against the // shipped types rather than absorbed by `BaseSchema`'s index signature. @@ -418,22 +428,24 @@ const main: ContainerSchema = { }; // An authored `flex` node takes its props, the child list included, in its -// `properties` bag; `satisfies FlexLayoutProps` checks each bag. -const page: BaseSchema = { +// `properties` bag. `FlexBlockNode` is that node, its bag closed. The bag's +// child list is `unknown[]`, as `FlexBlockSchema` declares it, so each child +// written inline names its own type with `satisfies`. +const page: FlexBlockNode = { type: 'flex', properties: { direction: 'col', children: [ - { type: 'header-bar', crumbs: [{ label: 'My App' }] }, + { type: 'header-bar', crumbs: [{ label: 'My App' }] } satisfies HeaderBarSchema, { type: 'flex', properties: { direction: 'row', children: [sidebar, main] - } satisfies FlexLayoutProps - } + } + } satisfies FlexBlockNode ] - } satisfies FlexLayoutProps + } }; ``` From 333f5a89d20f9a259483053a9153efda38649e6a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 14:49:53 +0000 Subject: [PATCH 4/4] test(types): pin the AuthoringNode join with an inline literal, not a structural fit (objectui#11468) A typed FlexBlockNode constant fits BaseSchema-without-its-index-signature structurally whether or not AuthoringNode includes the carriers, so that assertion could not fail. An inline flex literal where SchemaRenderer takes a schema compiles only through AuthoringNode. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- .../authoring-nodes-bag-carriers-11468.test.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts b/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts index 6ff92fe483..f3b5328330 100644 --- a/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts +++ b/packages/types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts @@ -125,11 +125,6 @@ type _PublicBlocksUnchanged = Assert< Equal> >; -// 5. Every new node type is accepted where `SchemaRenderer` takes a schema, after the removal too. -type _EveryCarrierFitsThePostRemovalProp = Assert< - ObjectQLPublicBlockNode | FlexBlockNode extends PostRemovalSchema ? true : false ->; - describe('objectui#11468: the authored properties-bag carriers have a TypeScript authoring face', () => { it('object-map: the spec row is the bag, a misspelled bag key and a flat prop are refused, after the removal', () => { const spelled: PostRemovalSchema = { @@ -157,8 +152,10 @@ describe('objectui#11468: the authored properties-bag carriers have a TypeScript const nodeChildren: FlexBlockNode = { type: 'flex', children: [] }; // @ts-expect-error `body` is refused toward `properties.children` const body: FlexBlockNode = { type: 'flex', body: [] }; - const asProp: PostRemovalSchema = box; - expect([box, misspelled, flat, nodeChildren, body, asProp]).toHaveLength(6); + // An inline literal where `SchemaRenderer` takes a schema compiles after the removal, through + // `AuthoringNode` (a typed constant like `box` would fit `BaseSchema` structurally either way). + const inline: PostRemovalSchema = { type: 'flex', properties: { direction: 'row', gap: 2 } }; + expect([box, misspelled, flat, nodeChildren, body, inline]).toHaveLength(6); }); it('object-chart: the bag is closed, though it is no spec row', () => {