diff --git a/builder/src/types.ts b/builder/src/types.ts index ef39b60f4..ee7c7fe08 100644 --- a/builder/src/types.ts +++ b/builder/src/types.ts @@ -1,7 +1,7 @@ import { isObj } from "@commontools/utils"; -export const ID: symbol = Symbol("ID, unique to the context"); -export const ID_FIELD: symbol = Symbol( +export const ID: unique symbol = Symbol("ID, unique to the context"); +export const ID_FIELD: unique symbol = Symbol( "ID_FIELD, name of sibling that contains id", ); @@ -108,9 +108,11 @@ export type JSONValue = | boolean | null | JSONValue[] - | { [key: string]: JSONValue }; + | { [key: string]: JSONValue } & { [ID]?: any; [ID_FIELD]?: any }; export type JSONSchema = { + readonly [ID]?: any; + readonly [ID_FIELD]?: any; readonly type?: | "object" | "array" diff --git a/runner/src/cell.ts b/runner/src/cell.ts index 3ea5c7ecd..70749e4dc 100644 --- a/runner/src/cell.ts +++ b/runner/src/cell.ts @@ -1,5 +1,10 @@ import { isStreamAlias, TYPE } from "@commontools/builder"; -import { getTopFrame, ID, type JSONSchema } from "@commontools/builder"; +import { + getTopFrame, + ID, + ID_FIELD, + type JSONSchema, +} from "@commontools/builder"; import { type DeepKeyLookup, type DocImpl, getDoc, isDoc } from "./doc.ts"; import { createQueryResultProxy, @@ -103,15 +108,15 @@ import { type Schema } from "@commontools/builder"; */ export interface Cell { get(): T; - set(value: T): void; - send(value: T): void; - update(values: Partial): void; + set(value: Cellify | T): void; + send(value: Cellify | T): void; + update | Partial>>( + values: V extends object ? V : never, + ): void; push( ...value: Array< - | (T extends Array ? U : any) - | DocImpl ? U : any> + | (T extends Array ? (Cellify | U | DocImpl) : any) | CellLink - | Cell ? U : any> > ): void; equals(other: Cell): boolean; @@ -164,6 +169,26 @@ export interface Cell { copyTrap: boolean; } +/** + * Cellify is a type utility that allows any part of type T to be wrapped in + * Cell<>, and allow any part of T that is currently wrapped in Cell<> to be + * used unwrapped. This is designed for use with Cell method parameters, + * allowing flexibility in how values are passed. + */ +export type Cellify = + // Handle existing Cell<> types, allowing unwrapping + T extends Cell ? Cellify | Cell> + // Handle arrays + : T extends Array ? Array> | Cell>> + // Handle objects (excluding null), adding optional ID fields + : T extends object ? + | ({ [K in keyof T]: Cellify } & { [ID]?: any; [ID_FIELD]?: any }) + | Cell< + { [K in keyof T]: Cellify } & { [ID]?: any; [ID_FIELD]?: any } + > + // Handle primitives + : T | Cell; + export interface Stream { send(event: T): void; sink(callback: (event: T) => Cancel | undefined | void): Cancel; @@ -359,15 +384,15 @@ function createRegularCell( const self = { get: () => validateAndTransform(doc, path, schema, log, rootSchema), - set: (newValue: T) => + set: (newValue: Cellify) => diffAndUpdate( resolveLinkToAlias(doc, path, log), newValue, log, getTopFrame()?.cause, ), - send: (newValue: T) => self.set(newValue), - update: (values: Partial) => { + send: (newValue: Cellify) => self.set(newValue), + update: (values: Cellify>) => { if (typeof values !== "object" || values === null) { throw new Error("Can't update with non-object value"); } @@ -378,10 +403,8 @@ function createRegularCell( }, push: ( ...values: Array< - | (T extends Array ? U : any) - | DocImpl ? U : any> + | (T extends Array ? (Cellify | U | DocImpl) : any) | CellLink - | Cell ? U : any> > ) => { // Follow aliases and references, since we want to get to an assumed diff --git a/runner/test/cell.test.ts b/runner/test/cell.test.ts index d5a50c7ba..a34e24379 100644 --- a/runner/test/cell.test.ts +++ b/runner/test/cell.test.ts @@ -4,13 +4,7 @@ import { type DocImpl, getDoc, isDoc } from "../src/doc.ts"; import { isCell, isCellLink } from "../src/cell.ts"; import { isQueryResult } from "../src/query-result-proxy.ts"; import { type ReactivityLog } from "../src/scheduler.ts"; -import { - getTopFrame, - ID, - JSONSchema, - popFrame, - pushFrame, -} from "@commontools/builder"; +import { ID, JSONSchema, popFrame, pushFrame } from "@commontools/builder"; import { addEventHandler, idle } from "../src/scheduler.ts"; import { addCommonIDfromObjectID } from "../src/utils.ts"; @@ -562,7 +556,7 @@ describe("asCell with schema", () => { }, }, required: ["name", "age", "tags", "nested"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -596,7 +590,7 @@ describe("asCell with schema", () => { }, }, required: ["id", "metadata"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const value = c.asCell([], undefined, schema).get(); @@ -643,7 +637,7 @@ describe("asCell with schema", () => { }, }, required: ["name", "children"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const value = c.asCell([], undefined, schema).get(); @@ -700,7 +694,7 @@ describe("asCell with schema", () => { }, }, required: ["user"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const userCell = cell.key("user"); @@ -831,7 +825,7 @@ describe("asCell with schema", () => { }, }, required: ["id", "context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -873,7 +867,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -919,7 +913,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -965,7 +959,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const value = cell.get(); @@ -1032,7 +1026,7 @@ describe("asCell with schema", () => { }, }, required: ["context"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const log = { reads: [], writes: [] } as ReactivityLog; const cell = c.asCell([], log, schema); @@ -1082,7 +1076,7 @@ describe("asCell with schema", () => { }, }, required: ["items"], - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); const itemsCell = cell.key("items"); @@ -1115,7 +1109,7 @@ describe("asCell with schema", () => { value: { type: "number" }, }, }, - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); @@ -1149,7 +1143,7 @@ describe("asCell with schema", () => { type: "object", properties: { anything: { asCell: true } }, }, - } satisfies JSONSchema; + } as const satisfies JSONSchema; const cell = c.asCell([], undefined, schema); @@ -1226,8 +1220,8 @@ describe("asCell with schema", () => { const schema = { type: "array", items: { type: "object", properties: { value: { type: "number" } } }, - default: [{ [ID]: "test", "value": 10 }, { [ID]: "test2", "value": 20 }], - } as JSONSchema; + default: [{ [ID]: "test", value: 10 }, { [ID]: "test2", value: 20 }], + } as const satisfies JSONSchema; const c = getDoc({}, "push-to-undefined-schema-stable-id", "test"); const arrayCell = c.asCell(["items"], undefined, schema);