From a138ec681a9844bd5d8e9b2676e5e52ac1034f02 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Thu, 28 Aug 2025 11:06:02 -0400 Subject: [PATCH 1/8] First pass at ColumnBuilder and TypeBuilder --- crates/bindings-typescript/src/index.ts | 1 + .../src/server/reducers.ts | 50 ++ .../bindings-typescript/src/server/schema.ts | 699 +++++++++++++++++ .../src/server/type_builders.ts | 707 ++++++++++++++++++ .../src/server/type_util.ts | 5 + crates/bindings-typescript/test/index.test.ts | 22 + crates/bindings-typescript/vitest.config.ts | 9 + 7 files changed, 1493 insertions(+) create mode 100644 crates/bindings-typescript/src/server/reducers.ts create mode 100644 crates/bindings-typescript/src/server/schema.ts create mode 100644 crates/bindings-typescript/src/server/type_builders.ts create mode 100644 crates/bindings-typescript/src/server/type_util.ts create mode 100644 crates/bindings-typescript/test/index.test.ts create mode 100644 crates/bindings-typescript/vitest.config.ts diff --git a/crates/bindings-typescript/src/index.ts b/crates/bindings-typescript/src/index.ts index 8f22f4c3048..1336f9b9d6c 100644 --- a/crates/bindings-typescript/src/index.ts +++ b/crates/bindings-typescript/src/index.ts @@ -8,3 +8,4 @@ export * from './time_duration'; export * from './timestamp'; export * from './utils'; export * from './identity'; +export { default as t } from './server/type_builders'; diff --git a/crates/bindings-typescript/src/server/reducers.ts b/crates/bindings-typescript/src/server/reducers.ts new file mode 100644 index 00000000000..f79a193c05a --- /dev/null +++ b/crates/bindings-typescript/src/server/reducers.ts @@ -0,0 +1,50 @@ +// import { +// clientConnected, +// clientDisconnected, +// init, +// player, +// point, +// procedure, +// reducer, +// sendMessageSchedule, +// user, +// type Schema, +// } from './schema'; + +// export const sendMessage = reducer( +// 'send_message', +// sendMessageSchedule, +// (ctx, { scheduleId, scheduledAt, text }) => { +// console.log(`Sending message: ${text} ${scheduleId}`); +// } +// ); + +// init('init', {}, ctx => { +// console.log('Database initialized'); +// }); + +// clientConnected('on_connect', {}, ctx => { +// console.log('Client connected'); +// }); + +// clientDisconnected('on_disconnect', {}, ctx => { +// console.log('Client disconnected'); +// }); + +// reducer( +// 'move_player', +// { user, foo: point, player }, +// (ctx: ReducerCtx, user, foo, player): void => { +// if (player.baz.tag === 'Foo') { +// player.baz.value += 1; +// } else if (player.baz.tag === 'Bar') { +// player.baz.value += 2; +// } else if (player.baz.tag === 'Baz') { +// player.baz.value += '!'; +// } +// } +// ); + +// procedure('get_user', { user }, async (ctx, { user }) => { +// console.log(user.email); +// }); diff --git a/crates/bindings-typescript/src/server/schema.ts b/crates/bindings-typescript/src/server/schema.ts new file mode 100644 index 00000000000..dc75d26bc9e --- /dev/null +++ b/crates/bindings-typescript/src/server/schema.ts @@ -0,0 +1,699 @@ +// import { +// AlgebraicType, +// ProductType, +// ProductTypeElement, +// } from '../algebraic_type'; +// import type RawConstraintDefV9 from '../autogen/raw_constraint_def_v_9_type'; +// import RawIndexAlgorithm from '../autogen/raw_index_algorithm_type'; +// import type RawIndexDefV9 from '../autogen/raw_index_def_v_9_type'; +// import { RawModuleDefV9 } from "../autogen/raw_module_def_v_9_type"; +// import type RawReducerDefV9 from '../autogen/raw_reducer_def_v_9_type'; +// import type RawSequenceDefV9 from '../autogen/raw_sequence_def_v_9_type'; +// import Lifecycle from '../autogen/lifecycle_type'; +// import ScheduleAt from '../schedule_at'; +// import RawTableDefV9 from '../autogen/raw_table_def_v_9_type'; +// import type Typespace from '../autogen/typespace_type'; +// import type { ColumnBuilder } from './type_builders'; +// import t from "./type_builders"; + +// type AlgebraicTypeRef = number; +// type ColId = number; +// type ColList = ColId[]; + +// /***************************************************************** +// * shared helpers +// *****************************************************************/ +// type Merge = M1 & Omit; +// type Values = T[keyof T]; + +// /***************************************************************** +// * the run‑time catalogue that we are filling +// *****************************************************************/ +// export const MODULE_DEF: RawModuleDefV9 = { +// typespace: { types: [] }, +// tables: [], +// reducers: [], +// types: [], +// miscExports: [], +// rowLevelSecurity: [], +// }; + + +// /***************************************************************** +// * Type helpers +// *****************************************************************/ +// type ColumnType = C extends ColumnBuilder ? JS : never; +// export type Infer = S extends ColumnBuilder ? JS : never; + +// /***************************************************************** +// * Index helper type used *inside* table() to enforce that only +// * existing column names are referenced. +// *****************************************************************/ +// type PendingIndex = { +// name?: string; +// accessor_name?: string; +// algorithm: +// | { tag: 'BTree'; value: { columns: readonly AllowedCol[] } } +// | { tag: 'Hash'; value: { columns: readonly AllowedCol[] } } +// | { tag: 'Direct'; value: { column: AllowedCol } }; +// }; + +// /***************************************************************** +// * table() +// *****************************************************************/ +// type TableOpts< +// N extends string, +// Def extends Record>, +// Idx extends PendingIndex[] | undefined = undefined, +// > = { +// name: N; +// public?: boolean; +// indexes?: Idx; // declarative multi‑column indexes +// scheduled?: string; // reducer name for cron‑like tables +// }; + +// /***************************************************************** +// * Branded types for better IDE navigation +// *****************************************************************/ + +// // Create unique symbols for each table to enable better IDE navigation +// declare const TABLE_BRAND: unique symbol; +// declare const SCHEMA_BRAND: unique symbol; + +// /***************************************************************** +// * Opaque handle that `table()` returns, now remembers the NAME literal +// *****************************************************************/ + +// // Helper types for extracting info from table handles +// type RowOf = H extends TableHandle ? R : never; +// type NameOf = H extends TableHandle ? N : never; + +// /***************************************************************** +// * table() – unchanged behavior, but return typed Name on the handle +// *****************************************************************/ +// /** +// * Defines a database table with schema and options +// * @param opts - Table configuration including name, indexes, and access control +// * @param row - Product type defining the table's row structure +// * @returns Table handle for use in schema() function +// * @example +// * ```ts +// * const playerTable = table( +// * { name: 'player', public: true }, +// * t.object({ +// * id: t.u32().primary_key(), +// * name: t.string().index('btree') +// * }) +// * ); +// * ``` +// */ +// export function table< +// const TableName extends string, +// Row extends Record>, +// Idx extends PendingIndex[] | undefined = undefined, +// >(opts: TableOpts, row: Row): TableHandle> { +// const { +// name, +// public: isPublic = false, +// indexes: userIndexes = [], +// scheduled, +// } = opts; + +// /** 1. column catalogue + helpers */ +// const def = row.__def__; +// const colIds = new Map(); +// const colIdList: ColList = []; + +// let nextCol: number = 0; +// for (const colName of Object.keys(def) as (keyof Row & string)[]) { +// colIds.set(colName, nextCol++); +// colIdList.push(colIds.get(colName)!); +// } + +// /** 2. gather primary keys, per‑column indexes, uniques, sequences */ +// const pk: ColList = []; +// const indexes: RawIndexDefV9[] = []; +// const constraints: RawConstraintDefV9[] = []; +// const sequences: RawSequenceDefV9[] = []; + +// let scheduleAtCol: ColId | undefined; + +// for (const [name, builder] of Object.entries(def) as [ +// keyof Row & string, +// ColumnBuilder, +// ][]) { +// const meta: any = builder.__meta__; + +// /* primary key */ +// if (meta.primaryKey) pk.push(colIds.get(name)!); + +// /* implicit 1‑column indexes */ +// if (meta.index) { +// const algo = (meta.index ?? 'btree') as 'BTree' | 'Hash' | 'Direct'; +// const id = colIds.get(name)!; +// indexes.push( +// algo === 'Direct' +// ? { name: "TODO", accessorName: "TODO", algorithm: RawIndexAlgorithm.Direct(id) } +// : { name: "TODO", accessorName: "TODO", algorithm: { tag: algo, value: [id] } } +// ); +// } + +// /* uniqueness */ +// if (meta.unique) { +// constraints.push({ +// name: "TODO", +// data: { tag: 'Unique', value: { columns: [colIds.get(name)!] } }, +// }); +// } + +// /* auto increment */ +// if (meta.autoInc) { +// sequences.push({ +// name: "TODO", +// start: 0n, // TODO +// minValue: 0n, // TODO +// maxValue: 0n, // TODO +// column: colIds.get(name)!, +// increment: 1n, +// }); +// } + +// /* scheduleAt */ +// if (meta.scheduleAt) scheduleAtCol = colIds.get(name)!; +// } + +// /** 3. convert explicit multi‑column indexes coming from options.indexes */ +// for (const pending of (userIndexes ?? []) as PendingIndex< +// keyof Row & string +// >[]) { +// const converted: RawIndexDefV9 = { +// name: pending.name, +// accessorName: pending.accessor_name, +// algorithm: ((): RawIndexAlgorithm => { +// if (pending.algorithm.tag === 'Direct') +// return { +// tag: 'Direct', +// value: colIds.get(pending.algorithm.value.column)!, +// }; +// return { +// tag: pending.algorithm.tag, +// value: pending.algorithm.value.columns.map(c => colIds.get(c)!), +// }; +// })(), +// }; +// indexes.push(converted); +// } + +// /** 4. add the product type to the global Typespace */ +// const productTypeRef: AlgebraicTypeRef = MODULE_DEF.typespace.types.length; +// MODULE_DEF.typespace.types.push(row.__spacetime_type__); + +// /** 5. finalise table record */ +// const tableDef: RawTableDefV9 = { +// name, +// productTypeRef, +// primaryKey: pk, +// indexes, +// constraints, +// sequences, +// schedule: +// scheduled && scheduleAtCol !== undefined +// ? { +// name: "TODO", +// reducerName: scheduled, +// scheduledAtColumn: scheduleAtCol, +// } +// : undefined, +// tableType: { tag: 'User' }, +// tableAccess: { tag: isPublic ? 'Public' : 'Private' }, +// }; +// MODULE_DEF.tables.push(tableDef); + +// return { +// __table_name__: name as TableName, +// __row_type__: {} as Infer, +// __row_spacetime_type__: row.__spacetime_type__, +// } as TableHandle>; +// } + +// /***************************************************************** +// * schema() – Fixed to properly infer table names and row types +// *****************************************************************/ + +// /***************************************************************** +// * reducer() +// *****************************************************************/ +// type ParamsAsObject>> = { +// [K in keyof ParamDef]: Infer; +// }; + +// /***************************************************************** +// * procedure() +// * +// * Stored procedures are opaque to the DB engine itself, so we just +// * keep them out of `RawModuleDefV9` for now – you can forward‑declare +// * a companion `RawMiscModuleExportV9` type later if desired. +// *****************************************************************/ +// export function procedure< +// Name extends string, +// Params extends Record>, +// Ctx, +// R, +// >( +// _name: Name, +// _params: Params, +// _fn: (ctx: Ctx, payload: ParamsAsObject) => Promise | R +// ): void { +// /* nothing to push yet — left for your misc export section */ +// } + +// /***************************************************************** +// * internal: pushReducer() helper used by reducer() and lifecycle wrappers +// *****************************************************************/ +// function pushReducer< +// S, +// Name extends string = string, +// Params extends Record> = Record< +// string, +// ColumnBuilder +// >, +// >( +// name: Name, +// params: Params | ProductTypeColumnBuilder, +// lifecycle?: RawReducerDefV9['lifecycle'] +// ): void { +// // Allow either a product-type ColumnBuilder or a plain params object +// const paramsInternal: Params = +// (params as any).__is_product_type__ === true +// ? (params as ProductTypeColumnBuilder).__def__ +// : (params as Params); + +// const paramType = { +// elements: Object.entries(paramsInternal).map( +// ([n, c]) => +// ({ name: n, algebraicType: (c as ColumnBuilder).__spacetime_type__ }) +// ) +// }; + +// MODULE_DEF.reducers.push({ +// name, +// params: paramType, +// lifecycle, // <- lifecycle flag lands here +// }); +// } + +// /***************************************************************** +// * reducer() – leave behavior the same; delegate to pushReducer() +// *****************************************************************/ + + +// /***************************************************************** +// * Lifecycle reducers +// * - register with lifecycle: 'init' | 'on_connect' | 'on_disconnect' +// * - keep the same call shape you're already using +// *****************************************************************/ +// export function init< +// S extends Record = any, +// Params extends Record> = {}, +// >( +// name: 'init' = 'init', +// params: Params | ProductTypeColumnBuilder = {} as any, +// _fn?: (ctx: ReducerCtx, payload: ParamsAsObject) => void +// ): void { +// pushReducer(name, params, Lifecycle.Init); +// } + +// export function clientConnected< +// S extends Record = any, +// Params extends Record> = {}, +// >( +// name: 'on_connect' = 'on_connect', +// params: Params | ProductTypeColumnBuilder = {} as any, +// _fn?: (ctx: ReducerCtx, payload: ParamsAsObject) => void +// ): void { +// pushReducer(name, params, Lifecycle.OnConnect); +// } + +// export function clientDisconnected< +// S extends Record = any, +// Params extends Record> = {}, +// >( +// name: 'on_disconnect' = 'on_disconnect', +// params: Params | ProductTypeColumnBuilder = {} as any, +// _fn?: (ctx: ReducerCtx, payload: ParamsAsObject) => void +// ): void { +// pushReducer(name, params, Lifecycle.OnDisconnect); +// } + +// /***************************************************************** +// * Example usage with explicit interfaces for better navigation +// *****************************************************************/ +// const point = t.object({ +// x: t.f64(), +// y: t.f64(), +// }); +// type Point = Infer; + +// const user = { +// id: t.string().primaryKey(), +// name: t.string().index('btree'), +// email: t.string(), +// age: t.number(), +// }; +// type User = Infer; + +// const player = { +// id: t.u32().primaryKey().autoInc(), +// name: t.string().index('btree'), +// score: t.number(), +// level: t.number(), +// foo: t.number().unique(), +// bar: t.object({ +// x: t.f64(), +// y: t.f64(), +// }), +// baz: t.enum({ +// Foo: t.f64(), +// Bar: t.f64(), +// Baz: t.string(), +// }), +// }; + + +// const sendMessageSchedule = t.object({ +// scheduleId: t.u64().primaryKey(), +// scheduledAt: t.scheduleAt(), +// text: t.string(), +// }); + +// // Create the schema with named references +// const s = schema( +// table({ +// name: 'player', +// public: true, +// indexes: [ +// t.index({ name: 'my_index' }).btree({ columns: ['name', 'score'] }), +// ], +// }, player), +// table({ name: 'logged_out_user' }, user), +// table({ name: 'user' }, user), +// table({ +// name: 'send_message_schedule', +// scheduled: 'move_player', +// }, sendMessageSchedule) +// ); + +// // Export explicit type alias for the schema +// export type Schemar = InferSchema; + +// const foo = reducer('move_player', { user, point, player }, (ctx, { user, point, player }) => { +// ctx.db.send_message_schedule.insert({ +// scheduleId: 1, +// scheduledAt: ScheduleAt.Interval(234_000n), +// text: 'Move player' +// }); + +// ctx.db.player.insert(player); + +// if (player.baz.tag === 'Foo') { +// player.baz.value += 1; +// } else if (player.baz.tag === 'Bar') { +// player.baz.value += 2; +// } else if (player.baz.tag === 'Baz') { +// player.baz.value += '!'; +// } +// }); + +// const bar = reducer('foobar', {}, (ctx) => { +// bar(ctx, {}); +// }) + +// init('init', {}, (_ctx) => { + +// }) + +// // Result like Rust +// export type Result = +// | { ok: true; value: T } +// | { ok: false; error: E }; + +// // /* ───── generic index‑builder to be used in table options ───── */ +// // index(opts?: { +// // name?: IdxName; +// // }): { +// // btree(def: { +// // columns: Cols; +// // }): PendingIndex<(typeof def.columns)[number]>; +// // hash(def: { +// // columns: Cols; +// // }): PendingIndex<(typeof def.columns)[number]>; +// // direct(def: { column: Col }): PendingIndex; +// // } { +// // const common = { name: opts?.name }; +// // return { +// // btree(def: { columns: Cols }) { +// // return { +// // ...common, +// // algorithm: { +// // tag: 'BTree', +// // value: { columns: def.columns }, +// // }, +// // } as PendingIndex<(typeof def.columns)[number]>; +// // }, +// // hash(def: { columns: Cols }) { +// // return { +// // ...common, +// // algorithm: { +// // tag: 'Hash', +// // value: { columns: def.columns }, +// // }, +// // } as PendingIndex<(typeof def.columns)[number]>; +// // }, +// // direct(def: { column: Col }) { +// // return { +// // ...common, +// // algorithm: { +// // tag: 'Direct', +// // value: { column: def.column }, +// // }, +// // } as PendingIndex; +// // }, +// // }; +// // }, + +// // type TableOpts< +// // N extends string, +// // Def extends Record>, +// // Idx extends PendingIndex[] | undefined = undefined, +// // > = { +// // name: N; +// // public?: boolean; +// // indexes?: Idx; // declarative multi‑column indexes +// // scheduled?: string; // reducer name for cron‑like tables +// // }; + + +// // export function table< +// // const Name extends string, +// // Def extends Record>, +// // Row extends ProductTypeColumnBuilder, +// // Idx extends PendingIndex[] | undefined = undefined, +// // >(opts: TableOpts, row: Row): TableHandle, Name> { + + + +// // /** +// // * Creates a schema from table definitions +// // * @param handles - Array of table handles created by table() function +// // * @returns ColumnBuilder representing the complete database schema +// // * @example +// // * ```ts +// // * const s = schema( +// // * table({ name: 'users' }, userTable), +// // * table({ name: 'posts' }, postTable) +// // * ); +// // * ``` +// // */ +// // export function schema< +// // const H extends readonly TableHandle[] +// // >(...handles: H): ColumnBuilder> & { +// // /** @internal - for IDE navigation to schema variable */ +// // readonly __schema_definition__?: never; +// // }; + +// // /** +// // * Creates a schema from table definitions (array overload) +// // * @param handles - Array of table handles created by table() function +// // * @returns ColumnBuilder representing the complete database schema +// // */ +// // export function schema< +// // const H extends readonly TableHandle[] +// // >(handles: H): ColumnBuilder> & { +// // /** @internal - for IDE navigation to schema variable */ +// // readonly __schema_definition__?: never; +// // }; + +// // export function schema(...args: any[]): ColumnBuilder { +// // const handles = +// // (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as TableHandle[]; + +// // const productTy = AlgebraicType.Product({ +// // elements: handles.map(h => ({ +// // name: h.__table_name__, +// // algebraicType: h.__row_spacetime_type__, +// // })), +// // }); + +// // return col(productTy); +// // } + +// type UntypedTablesTuple = TableHandle[]; +// function schema(...tablesTuple: TablesTuple): Schema { +// return { +// tables: tablesTuple +// } +// } + +// type UntypedSchemaDef = { +// typespace: Typespace, +// tables: [RawTableDefV9], +// } + +// type Schema = { +// tables: Tables, +// } + +// type TableHandle = { +// readonly __table_name__: TableName; +// readonly __row_type__: Row; +// readonly __row_spacetime_type__: AlgebraicType; +// }; + +// type InferSchema = SchemaDef extends Schema ? Tables : never; + +// /** +// * Reducer context parametrized by the inferred Schema +// */ +// export type ReducerCtx = { +// db: DbView; +// }; + + +// type DbView = { +// [K in keyof SchemaDef]: Table> +// }; + + +// // schema provided -> ctx.db is precise +// export function reducer< +// S extends Record, +// Name extends string = string, +// Params extends Record> = Record< +// string, +// ColumnBuilder +// >, +// F = (ctx: ReducerCtx, payload: ParamsAsObject) => void, +// >(name: Name, params: Params | ProductTypeColumnBuilder, fn: F): F; + +// // no schema provided -> ctx.db is permissive +// export function reducer< +// Name extends string = string, +// Params extends Record> = Record< +// string, +// ColumnBuilder +// >, +// F = (ctx: ReducerCtx, payload: ParamsAsObject) => void, +// >(name: Name, params: Params | ProductTypeColumnBuilder, fn: F): F; + +// // single implementation (S defaults to any -> JS-like) +// export function reducer< +// S extends Record = any, +// Name extends string = string, +// Params extends Record> = Record< +// string, +// ColumnBuilder +// >, +// F = (ctx: ReducerCtx, payload: ParamsAsObject) => void, +// >(name: Name, params: Params | ProductTypeColumnBuilder, fn: F): F { +// pushReducer(name, params); +// return fn; +// } + + + + + + + +// // export type Infer = S extends ColumnBuilder ? JS : never; + +// // Create interfaces for each table to enable better navigation +// type TableHandleTupleToObject[]> = +// T extends readonly [TableHandle, ...infer Rest] +// ? Rest extends readonly TableHandle[] +// ? { [K in N1]: R1 } & TableHandleTupleToObject +// : { [K in N1]: R1 } +// : {}; + +// // Alternative approach - direct tuple iteration with interfaces +// type TupleToSchema[]> = TableHandleTupleToObject; + +// type TableNamesInSchemaDef = +// keyof SchemaDef & string; + +// type TableByName< +// SchemaDef extends UntypedSchemaDef, +// TableName extends TableNamesInSchemaDef, +// > = SchemaDef[TableName]; + +// type RowFromTable = +// TableDef["row"]; + +// /** +// * Reducer context parametrized by the inferred Schema +// */ +// type ReducerContext = { +// db: DbView; +// }; + +// type AnyTable = Table; +// type AnySchema = Record; + +// type Outer = { + +// } + +// type ReducerBuilder = { + +// } + +// type Local = {}; + +// /** +// * Table +// * +// * - Row: row shape +// * - UCV: unique-constraint violation error type (never if none) +// * - AIO: auto-increment overflow error type (never if none) +// */ +// export type Table = { +// /** Returns the number of rows in the TX state. */ +// count(): number; + +// /** Iterate over all rows in the TX state. Rust IteratorIterator → TS Iterable. */ +// iter(): IterableIterator; + +// /** Insert and return the inserted row (auto-increment fields filled). May throw on error. */ +// insert(row: Row): Row; + +// /** Like insert, but returns a Result instead of throwing. */ +// try_insert(row: Row): Result; + +// /** Delete a row equal to `row`. Returns true if something was deleted. */ +// delete(row: Row): boolean; +// }; + + +// type DbContext> = { +// db: DbView, +// }; diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts new file mode 100644 index 00000000000..67e8c522356 --- /dev/null +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -0,0 +1,707 @@ +import { AlgebraicType, ProductTypeElement, ScheduleAt, SumTypeVariant, type AlgebraicTypeVariants } from ".."; +import type __AlgebraicType from "../autogen/algebraic_type_type"; +import type { Prettify } from "./type_util"; + +/** + * A set of methods for building a column definition. Type builders extend this + * interface so that they can be converted to column builders by calling + * one of the methods that returns a column builder. + */ +interface IntoColumnBuilder { + /** + * Specify the index type for this column + * @param algorithm The index algorithm to use + */ + index( + algorithm?: N + ): ColumnBuilder & { indexType: N }>>; + + /** + * Specify this column as the primary key + */ + primaryKey(): ColumnBuilder & { isPrimaryKey: true }>>; + + /** + * Specify this column as unique + */ + unique(): ColumnBuilder & { isUnique: true }>>; + + /** + * Specify this column as auto-incrementing + */ + autoInc(): ColumnBuilder & { isAutoIncrement: true }>>; + + /** + * Specify this column as a schedule-at field + */ + scheduleAt(): ColumnBuilder & { isScheduleAt: true }>>; +} + +/** + * Helper type to extract the TypeScript type from a TypeBuilder + */ +type InferTypeOfTypeBuilder = T extends TypeBuilder ? U : never; + +/** + * Helper type to extract the TypeScript type from a TypeBuilder + */ +export type Infer = InferTypeOfTypeBuilder; + +/** + * Helper type to extract the type of a row from an object. + */ +type InferTypeOfRow = T extends Record | TypeBuilder> ? { [K in keyof T]: T[K] extends ColumnBuilder ? V : T[K] extends TypeBuilder ? V : never } : never; + +/** + * Type which represents a valid argument to the ProductColumnBuilder + */ +type ElementsObj = Record>; + +/** + * Type which converts the elements of ElementsObj to a ProductType elements array + */ +type ElementsArrayFromElementsObj = { name: keyof Obj & string; algebraicType: Obj[keyof Obj & string]["spacetimeType"] }[]; + +/** + * A type which converts the elements of ElementsObj to a TypeScript object type. + * It works by `Infer`ing the types of the column builders which are the values of + * the keys in the object passed in. + * + * e.g. { a: I32TypeBuilder, b: StringBuilder } -> { a: number, b: string } + */ +type TypeScriptTypeFromElementsObj = { + [K in keyof Elements]: InferTypeOfTypeBuilder +}; + +type VariantsObj = Record>; + +/** + * A type which converts the elements of ElementsObj to a TypeScript object type. + * It works by `Infer`ing the types of the column builders which are the values of + * the keys in the object passed in. + * + * e.g. { A: I32TypeBuilder, B: StringBuilder } -> { tag: "A", value: number } | { tag: "B", value: string } + */ +type TypeScriptTypeFromVariantsObj = { + [K in keyof Variants]: { tag: K; value: InferTypeOfTypeBuilder } +}[keyof Variants]; + +/** + * Type which converts the elements of VariantsObj to a SumType variants array + */ +type VariantsArrayFromVariantsObj = { name: keyof Obj & string; algebraicType: Obj[keyof Obj & string]["spacetimeType"] }[]; + +/** + * A generic type builder that captures both the TypeScript type + * and the corresponding `AlgebraicType`. + */ +export class TypeBuilder implements IntoColumnBuilder { + /** + * The TypeScript phantom type. This is not stored at runtime, + * but is visible to the compiler + */ + readonly type!: Type; + + /** + * TypeScript phantom type representing the type of the particular + * AlgebraicType stored in {@link algebraicType}. + */ + readonly spacetimeType!: SpacetimeType; + + /** + * The SpacetimeDB algebraic type (run‑time value). In addition to storing + * the runtime representation of the `AlgebraicType`, it also captures + * the TypeScript type information of the `AlgebraicType`. That is to say + * the value is not merely an `AlgebraicType`, but is constructed to be + * the corresponding concrete `AlgebraicType` for the TypeScript type `Type`. + * + * e.g. `string` corresponds to `AlgebraicType.String` + */ + readonly algebraicType: AlgebraicType; + + constructor(algebraicType: AlgebraicType) { + this.algebraicType = algebraicType; + } + + /** + * Specify the index type for this column + * @param algorithm The index algorithm to use + */ + index( + algorithm?: N + ): ColumnBuilder & { indexType: N }>> { + return new ColumnBuilder & { indexType: N }>>(this, { + ...defaultMetadata, + indexType: algorithm + }); + } + + /** + * Specify this column as the primary key + */ + primaryKey(): ColumnBuilder & { isPrimaryKey: true }>> { + return new ColumnBuilder & { isPrimaryKey: true }>>(this, { + ...defaultMetadata, + isPrimaryKey: true + }); + } + + /** + * Specify this column as unique + */ + unique(): ColumnBuilder & { isUnique: true }>> { + return new ColumnBuilder & { isUnique: true }>>(this, { + ...defaultMetadata, + isUnique: true + }); + } + + /** + * Specify this column as auto-incrementing + */ + autoInc(): ColumnBuilder & { isAutoIncrement: true }>> { + return new ColumnBuilder & { isAutoIncrement: true }>>(this, { + ...defaultMetadata, + isAutoIncrement: true + }); + } + + /** + * Specify this column as a schedule-at field + */ + scheduleAt(): ColumnBuilder & { isScheduleAt: true }>> { + return new ColumnBuilder & { isScheduleAt: true }>>(this, { + ...defaultMetadata, + isScheduleAt: true + }); + } +} + +export class U8Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.U8); + } +} +export class U16Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.U16); + } +} +export class U32Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.U32); + } +} +export class U64Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.U64); + } +} +export class U128Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.U128); + } +} +export class U256Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.U256); + } +} +export class I128Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.I128); + } +} +export class I256Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.I256); + } +} +export class F32Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.F32); + } +} +export class F64Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.F64); + } +} +export class BoolBuilder extends TypeBuilder { + constructor() { + super(AlgebraicType.Bool); + } +} +export class StringBuilder extends TypeBuilder { + constructor() { + super(AlgebraicType.String); + } +} +export class I8Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.I8); + } +} +export class I16Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.I16); + } +} +export class I32Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.I32); + } +} +export class I64Builder extends TypeBuilder { + constructor() { + super(AlgebraicType.I64); + } +} +export class ArrayBuilder> extends TypeBuilder, { tag: "Array", value: Element["spacetimeType"] }> { + /** + * The phantom element type of the array for TypeScript + */ + readonly element!: Element; + + constructor(element: Element) { + super(AlgebraicType.Array(element.algebraicType)); + } +} +export class ProductBuilder extends TypeBuilder, { tag: "Product", value: { elements: ElementsArrayFromElementsObj } }> { + /** + * The phantom element types of the product for TypeScript + */ + readonly elements!: Elements; + + constructor(elements: Elements) { + function elementsArrayFromElementsObj(obj: Obj) { + return Object.entries(obj).map(([name, { algebraicType }]) => ({ name, algebraicType })); + } + super(AlgebraicType.Product({ + elements: elementsArrayFromElementsObj(elements) + })); + } +} +export class SumBuilder extends TypeBuilder, { tag: "Sum", value: { variants: VariantsArrayFromVariantsObj } }> { + /** + * The phantom variant types of the sum for TypeScript + */ + readonly variants!: Variants; + + constructor(variants: Variants) { + function variantsArrayFromVariantsObj(variants: Variants): SumTypeVariant[] { + return Object.entries(variants).map(([name, { algebraicType }]) => ({ name, algebraicType })); + } + super(AlgebraicType.Sum({ + variants: variantsArrayFromVariantsObj(variants) + })); + } +} + +export interface U8ColumnBuilder extends ColumnBuilder { } +export interface U16ColumnBuilder extends ColumnBuilder { } +export interface U32ColumnBuilder extends ColumnBuilder { } +export interface U64ColumnBuilder extends ColumnBuilder { } +export interface U128ColumnBuilder extends ColumnBuilder { } +export interface U256ColumnBuilder extends ColumnBuilder { } +export interface I8ColumnBuilder extends ColumnBuilder { } +export interface I16ColumnBuilder extends ColumnBuilder { } +export interface I32ColumnBuilder extends ColumnBuilder { } +export interface I64ColumnBuilder extends ColumnBuilder { } +export interface I128ColumnBuilder extends ColumnBuilder { } +export interface I256ColumnBuilder extends ColumnBuilder { } +export interface F32ColumnBuilder extends ColumnBuilder { } +export interface F64ColumnBuilder extends ColumnBuilder { } +export interface BoolColumnBuilder extends ColumnBuilder { } +export interface StringColumnBuilder extends ColumnBuilder { } +export interface ArrayColumnBuilder> extends ColumnBuilder, { tag: "Array", value: Element["spacetimeType"] }> { } +export interface ProductColumnBuilder> extends ColumnBuilder<{ [K in Elements[number]['name']]: any }, { tag: "Product", value: { elements: Elements } }> { } +export interface SumColumnBuilder> extends ColumnBuilder<{ [K in Variants[number]['name']]: { tag: K; value: any } }[Variants[number]['name']], { tag: "Sum", value: { variants: Variants } }> { } + +/** + * The type of index types that can be applied to a column. + * `undefined` is the default + */ +type IndexTypes = "btree" | "hash" | undefined; + +/** + * Metadata describing column constraints and index type + */ +export type ColumnMetadata = { + isPrimaryKey: boolean; + isUnique: boolean; + isAutoIncrement: boolean; + isScheduleAt: boolean; + indexType: IndexTypes; +}; + +/** + * Default metadata state type for a newly created column + */ +type DefaultMetadata = { + isPrimaryKey: false; + isUnique: false; + isAutoIncrement: false; + isScheduleAt: false; + indexType: undefined; +}; + +/** + * Default metadata state value for a newly created column + */ +const defaultMetadata: DefaultMetadata = { + isPrimaryKey: false, + isUnique: false, + isAutoIncrement: false, + isScheduleAt: false, + indexType: undefined +}; + +/** + * A column builder allows you to incrementally specify constraints + * and metadata for a column in a type-safe way. + * + * It carries both a phantom TypeScript type (the `Type`) and + * runtime algebraic type information. + */ +export class ColumnBuilder< + Type, + SpacetimeType extends AlgebraicType, + M extends ColumnMetadata = DefaultMetadata +> implements IntoColumnBuilder { + typeBuilder: TypeBuilder; + columnMetadata: ColumnMetadata; + + constructor(typeBuilder: TypeBuilder, metadata?: ColumnMetadata) { + this.typeBuilder = typeBuilder; + this.columnMetadata = defaultMetadata; + } + + /** + * Specify the index type for this column + * @param algorithm The index algorithm to use + */ + index( + algorithm?: N + ): ColumnBuilder & { indexType: N }>> { + return new ColumnBuilder & { indexType: N }>>( + this.typeBuilder, + { + ...this.columnMetadata, + indexType: algorithm + } + ); + } + + /** + * Specify this column as the primary key + */ + primaryKey(): ColumnBuilder & { isPrimaryKey: true }>> { + return new ColumnBuilder & { isPrimaryKey: true }>>( + this.typeBuilder, + { + ...this.columnMetadata, + isPrimaryKey: true + } + ); + } + + /** + * Specify this column as unique + */ + unique(): ColumnBuilder & { isUnique: true }>> { + return new ColumnBuilder & { isUnique: true }>>( + this.typeBuilder, + { + ...this.columnMetadata, + isUnique: true + } + ); + } + + /** + * Specify this column as auto-incrementing + */ + autoInc(): ColumnBuilder & { isAutoIncrement: true }>> { + return new ColumnBuilder & { isAutoIncrement: true }>>( + this.typeBuilder, + { + ...this.columnMetadata, + isAutoIncrement: true + } + ); + } + + /** + * Specify this column as a schedule-at field + */ + scheduleAt(): ColumnBuilder & { isScheduleAt: true }>> { + return new ColumnBuilder & { isScheduleAt: true }>>( + this.typeBuilder, + { + ...this.columnMetadata, + isScheduleAt: true + } + ); + } +} + +/** + * A collection of factory functions for creating various SpacetimeDB algebraic types + * to be used in table definitions. Each function returns a corresponding builder + * for a specific type, such as `BoolBuilder`, `StringBuilder`, or `F64Builder`. + * + * These builders are used to define the schema of tables in SpacetimeDB, and each + * builder implements the {@link TypeBuilder} interface, allowing for type-safe + * schema construction in TypeScript. + * + * @remarks + * - Primitive types (e.g., `bool`, `string`, `number`) map to their respective TypeScript types. + * - Integer and floating-point types (e.g., `i8`, `u64`, `f32`) are represented as `number` or `bigint` in TypeScript. + * - Complex types such as `object`, `array`, and `enum` allow for nested and structured schemas. + * - The `scheduleAt` builder is a special column type for scheduling. + * + * @see {@link TypeBuilder} + */ +const t = { + /** + * Creates a new `Bool` {@link AlgebraicType} to be used in table definitions + * Represented as `boolean` in TypeScript. + * @returns A new BoolBuilder instance + */ + bool: (): BoolBuilder => new BoolBuilder(), + + /** + * Creates a new `String` {@link AlgebraicType} to be used in table definitions + * Represented as `string` in TypeScript. + * @returns A new StringBuilder instance + */ + string: (): StringBuilder => new StringBuilder(), + + /** + * Creates a new `F64` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new F64Builder instance + */ + number: (): F64Builder => new F64Builder(), + + /** + * Creates a new `I8` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new I8Builder instance + */ + i8: (): I8Builder => new I8Builder(), + + /** + * Creates a new `U8` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new U8Builder instance + */ + u8: (): U8Builder => new U8Builder(), + + /** + * Creates a new `I16` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new I16Builder instance + */ + i16: (): I16Builder => new I16Builder(), + + /** + * Creates a new `U16` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new U16Builder instance + */ + u16: (): U16Builder => new U16Builder(), + + /** + * Creates a new `I32` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new I32Builder instance + */ + i32: (): I32Builder => new I32Builder(), + + /** + * Creates a new `U32` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new U32Builder instance + */ + u32: (): U32Builder => new U32Builder(), + + /** + * Creates a new `I64` AlgebraicType to be used in table definitions + * Represented as `bigint` in TypeScript. + * @returns A new U32Builder instance + */ + i64: (): I64Builder => new I64Builder(), + + /** + * Creates a new `U64` {@link AlgebraicType} to be used in table definitions + * Represented as `bigint` in TypeScript. + * @returns A new U64Builder instance + */ + u64: (): U64Builder => new U64Builder(), + + /** + * Creates a new `I128` {@link AlgebraicType} to be used in table definitions + * Represented as `bigint` in TypeScript. + * @returns A new I128Builder instance + */ + i128: (): I128Builder => new I128Builder(), + + /** + * Creates a new `U128` {@link AlgebraicType} to be used in table definitions + * Represented as `bigint` in TypeScript. + * @returns A new U128Builder instance + */ + u128: (): U128Builder => new U128Builder(), + + /** + * Creates a new `I256` {@link AlgebraicType} to be used in table definitions + * Represented as `bigint` in TypeScript. + * @returns A new I256Builder instance + */ + i256: (): I256Builder => new I256Builder(), + + /** + * Creates a new `U256` {@link AlgebraicType} to be used in table definitions + * Represented as `bigint` in TypeScript. + * @returns A new U256Builder instance + */ + u256: (): U256Builder => new U256Builder(), + + /** + * Creates a new `F32` AlgebraicType to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new F32Builder instance + */ + f32: (): F32Builder => new F32Builder(), + + /** + * Creates a new `F64` {@link AlgebraicType} to be used in table definitions + * Represented as `number` in TypeScript. + * @returns A new F64Builder instance + */ + f64: (): F64Builder => new F64Builder(), + + /** + * Creates a new `Object` AlgebraicType to be used in table definitions. + * Properties of the object must also be {@link TypeBuilder}s. + * Represented as an object with specific properties in TypeScript. + * @param obj The object defining the properties of the type, whose property + * values must be {@link TypeBuilder}s. + * @returns A new ObjectBuilder instance + */ + object( + obj: Obj + ): ProductBuilder { + return new ProductBuilder(obj); + }, + + /** + * Creates a new `Array` AlgebraicType to be used in table definitions. + * Represented as an array in TypeScript. + * @param element The element type of the array, which must be a `TypeBuilder`. + * @returns A new ArrayBuilder instance + */ + array>(e: Element): ArrayBuilder { + return new ArrayBuilder(e); + }, + + /** + * Creates a new `Enum` AlgebraicType to be used in table definitions. + * Represented as a union of string literals in TypeScript. + * @param obj The object defining the variants of the enum, whose variant + * types must be `TypeBuilder`s. + * @returns A new EnumBuilder instance + */ + enum( + obj: Obj + ): SumBuilder { + return new SumBuilder(obj); + }, + + /** + * This is a special helper function for conveniently creating {@link ScheduleAt} type columns. + * @returns A new ColumnBuilder instance with the {@link ScheduleAt} type. + */ + scheduleAt: (): ColumnBuilder, Omit & { isScheduleAt: true }> => { + return new ColumnBuilder, Omit & { isScheduleAt: true }>( + new TypeBuilder>(ScheduleAt.getAlgebraicType()), + { + ...defaultMetadata, + isScheduleAt: true + } + ); + }, +} as const; +export default t; + +// @typescript-eslint/no-unused-vars +namespace tests { + type MustBeNever = [T] extends [never] + ? true + : ["Error: Type must be never", T]; + + // Test type inference on a row + // i.e. a Record type + const row = { + foo: t.string(), + bar: t.i32().primaryKey(), + idx: t.i64().index("btree").unique() + }; + type Row = InferTypeOfRow; + const _row: Row = { + foo: "hello", + bar: 42, + idx: 100n + }; + + // Test that a row must not allow non-TypeBuilder or ColumnBuilder values + const row2 = { + foo: { + // bar is not a TypeBuilder or ColumnBuilder, so this should fail + bar: t.string() + }, + bar: t.i32().primaryKey(), + idx: t.i64().index("btree").unique() + }; + type Row2 = InferTypeOfRow; + type _ = MustBeNever; + + // Test type inference on a type with a nested object + const point = t.object({ + x: t.i32(), + y: t.f64(), + z: t.object({ + foo: t.string() + }) + }); + type Point = InferTypeOfTypeBuilder; + const _point: Point = { + x: 1.0, + y: 2.0, + z: { + foo: "bar" + } + }; + + // Test type inference on an enum + const e = t.enum({ + A: t.string(), + B: t.number() + }); + type E = InferTypeOfTypeBuilder; + const _e: E = { tag: "A", value: "hello" }; + const _e2: E = { tag: "B", value: 42 }; + + // Test that the type of a row includes the correct ColumnBuilder types + const _row3: { + foo: TypeBuilder; + bar: ColumnBuilder; + idx: ColumnBuilder; + } = { + foo: t.string(), + bar: t.i32().primaryKey(), + idx: t.i64().index("btree").unique() + }; +} diff --git a/crates/bindings-typescript/src/server/type_util.ts b/crates/bindings-typescript/src/server/type_util.ts new file mode 100644 index 00000000000..cf6518d4a51 --- /dev/null +++ b/crates/bindings-typescript/src/server/type_util.ts @@ -0,0 +1,5 @@ + +/** + * Utility to make TS show cleaner types by flattening intersections. + */ +export type Prettify = { [K in keyof T]: T[K] } & {}; \ No newline at end of file diff --git a/crates/bindings-typescript/test/index.test.ts b/crates/bindings-typescript/test/index.test.ts new file mode 100644 index 00000000000..29c41a9381d --- /dev/null +++ b/crates/bindings-typescript/test/index.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest' +import { AlgebraicType, t } from '../src/index'; + +describe('TypeBuilder', () => { + it('builds the correct algebraic type for a point', () => { + const point = t.object({ + x: t.f64(), + y: t.f64(), + z: t.f64() + }); + expect(point.algebraicType).toEqual({ + tag: 'Product', + value: { + elements: [ + { name: 'x', algebraicType: AlgebraicType.F64 }, + { name: 'y', algebraicType: AlgebraicType.F64 }, + { name: 'z', algebraicType: AlgebraicType.F64 } + ] + } + }); + }) +}) diff --git a/crates/bindings-typescript/vitest.config.ts b/crates/bindings-typescript/vitest.config.ts new file mode 100644 index 00000000000..3ca36ca5db3 --- /dev/null +++ b/crates/bindings-typescript/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + globals: true, + environment: 'node' + } +}) From 762c02624083425bd5365a0f59d73025a03800ac Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Thu, 28 Aug 2025 12:22:59 -0400 Subject: [PATCH 2/8] Requiring build before testing for the sdk --- crates/bindings-typescript/vitest.config.ts | 2 +- sdks/typescript/packages/sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bindings-typescript/vitest.config.ts b/crates/bindings-typescript/vitest.config.ts index 3ca36ca5db3..5147cfd5555 100644 --- a/crates/bindings-typescript/vitest.config.ts +++ b/crates/bindings-typescript/vitest.config.ts @@ -6,4 +6,4 @@ export default defineConfig({ globals: true, environment: 'node' } -}) +}) \ No newline at end of file diff --git a/sdks/typescript/packages/sdk/package.json b/sdks/typescript/packages/sdk/package.json index 2b1ab13f428..b851718c793 100644 --- a/sdks/typescript/packages/sdk/package.json +++ b/sdks/typescript/packages/sdk/package.json @@ -27,7 +27,7 @@ "build": "tsup", "format": "prettier --write .", "lint": "prettier . --check", - "test": "vitest run", + "test": "pnpm build && vitest run", "generate": "cargo build -p spacetimedb-standalone && cargo run -p spacetimedb-client-api-messages --example get_ws_schema > ws_schema.json && cargo run -p spacetimedb-cli generate --lang typescript --out-dir src/client_api --module-def ws_schema.json && rm ws_schema.json && find src/client_api -type f -exec perl -pi -e 's#\\@clockworklabs/spacetimedb-sdk#../index#g' {} + && prettier --write src/client_api", "size": "brotli-size dist/min/index.js" }, From 88ba5f02f303d93cb6b623c8effddb60be295e13 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Thu, 28 Aug 2025 12:24:27 -0400 Subject: [PATCH 3/8] Removed files I don't want in the PR --- .../src/server/reducers.ts | 50 -- .../bindings-typescript/src/server/schema.ts | 699 ------------------ 2 files changed, 749 deletions(-) delete mode 100644 crates/bindings-typescript/src/server/reducers.ts delete mode 100644 crates/bindings-typescript/src/server/schema.ts diff --git a/crates/bindings-typescript/src/server/reducers.ts b/crates/bindings-typescript/src/server/reducers.ts deleted file mode 100644 index f79a193c05a..00000000000 --- a/crates/bindings-typescript/src/server/reducers.ts +++ /dev/null @@ -1,50 +0,0 @@ -// import { -// clientConnected, -// clientDisconnected, -// init, -// player, -// point, -// procedure, -// reducer, -// sendMessageSchedule, -// user, -// type Schema, -// } from './schema'; - -// export const sendMessage = reducer( -// 'send_message', -// sendMessageSchedule, -// (ctx, { scheduleId, scheduledAt, text }) => { -// console.log(`Sending message: ${text} ${scheduleId}`); -// } -// ); - -// init('init', {}, ctx => { -// console.log('Database initialized'); -// }); - -// clientConnected('on_connect', {}, ctx => { -// console.log('Client connected'); -// }); - -// clientDisconnected('on_disconnect', {}, ctx => { -// console.log('Client disconnected'); -// }); - -// reducer( -// 'move_player', -// { user, foo: point, player }, -// (ctx: ReducerCtx, user, foo, player): void => { -// if (player.baz.tag === 'Foo') { -// player.baz.value += 1; -// } else if (player.baz.tag === 'Bar') { -// player.baz.value += 2; -// } else if (player.baz.tag === 'Baz') { -// player.baz.value += '!'; -// } -// } -// ); - -// procedure('get_user', { user }, async (ctx, { user }) => { -// console.log(user.email); -// }); diff --git a/crates/bindings-typescript/src/server/schema.ts b/crates/bindings-typescript/src/server/schema.ts deleted file mode 100644 index dc75d26bc9e..00000000000 --- a/crates/bindings-typescript/src/server/schema.ts +++ /dev/null @@ -1,699 +0,0 @@ -// import { -// AlgebraicType, -// ProductType, -// ProductTypeElement, -// } from '../algebraic_type'; -// import type RawConstraintDefV9 from '../autogen/raw_constraint_def_v_9_type'; -// import RawIndexAlgorithm from '../autogen/raw_index_algorithm_type'; -// import type RawIndexDefV9 from '../autogen/raw_index_def_v_9_type'; -// import { RawModuleDefV9 } from "../autogen/raw_module_def_v_9_type"; -// import type RawReducerDefV9 from '../autogen/raw_reducer_def_v_9_type'; -// import type RawSequenceDefV9 from '../autogen/raw_sequence_def_v_9_type'; -// import Lifecycle from '../autogen/lifecycle_type'; -// import ScheduleAt from '../schedule_at'; -// import RawTableDefV9 from '../autogen/raw_table_def_v_9_type'; -// import type Typespace from '../autogen/typespace_type'; -// import type { ColumnBuilder } from './type_builders'; -// import t from "./type_builders"; - -// type AlgebraicTypeRef = number; -// type ColId = number; -// type ColList = ColId[]; - -// /***************************************************************** -// * shared helpers -// *****************************************************************/ -// type Merge = M1 & Omit; -// type Values = T[keyof T]; - -// /***************************************************************** -// * the run‑time catalogue that we are filling -// *****************************************************************/ -// export const MODULE_DEF: RawModuleDefV9 = { -// typespace: { types: [] }, -// tables: [], -// reducers: [], -// types: [], -// miscExports: [], -// rowLevelSecurity: [], -// }; - - -// /***************************************************************** -// * Type helpers -// *****************************************************************/ -// type ColumnType = C extends ColumnBuilder ? JS : never; -// export type Infer = S extends ColumnBuilder ? JS : never; - -// /***************************************************************** -// * Index helper type used *inside* table() to enforce that only -// * existing column names are referenced. -// *****************************************************************/ -// type PendingIndex = { -// name?: string; -// accessor_name?: string; -// algorithm: -// | { tag: 'BTree'; value: { columns: readonly AllowedCol[] } } -// | { tag: 'Hash'; value: { columns: readonly AllowedCol[] } } -// | { tag: 'Direct'; value: { column: AllowedCol } }; -// }; - -// /***************************************************************** -// * table() -// *****************************************************************/ -// type TableOpts< -// N extends string, -// Def extends Record>, -// Idx extends PendingIndex[] | undefined = undefined, -// > = { -// name: N; -// public?: boolean; -// indexes?: Idx; // declarative multi‑column indexes -// scheduled?: string; // reducer name for cron‑like tables -// }; - -// /***************************************************************** -// * Branded types for better IDE navigation -// *****************************************************************/ - -// // Create unique symbols for each table to enable better IDE navigation -// declare const TABLE_BRAND: unique symbol; -// declare const SCHEMA_BRAND: unique symbol; - -// /***************************************************************** -// * Opaque handle that `table()` returns, now remembers the NAME literal -// *****************************************************************/ - -// // Helper types for extracting info from table handles -// type RowOf = H extends TableHandle ? R : never; -// type NameOf = H extends TableHandle ? N : never; - -// /***************************************************************** -// * table() – unchanged behavior, but return typed Name on the handle -// *****************************************************************/ -// /** -// * Defines a database table with schema and options -// * @param opts - Table configuration including name, indexes, and access control -// * @param row - Product type defining the table's row structure -// * @returns Table handle for use in schema() function -// * @example -// * ```ts -// * const playerTable = table( -// * { name: 'player', public: true }, -// * t.object({ -// * id: t.u32().primary_key(), -// * name: t.string().index('btree') -// * }) -// * ); -// * ``` -// */ -// export function table< -// const TableName extends string, -// Row extends Record>, -// Idx extends PendingIndex[] | undefined = undefined, -// >(opts: TableOpts, row: Row): TableHandle> { -// const { -// name, -// public: isPublic = false, -// indexes: userIndexes = [], -// scheduled, -// } = opts; - -// /** 1. column catalogue + helpers */ -// const def = row.__def__; -// const colIds = new Map(); -// const colIdList: ColList = []; - -// let nextCol: number = 0; -// for (const colName of Object.keys(def) as (keyof Row & string)[]) { -// colIds.set(colName, nextCol++); -// colIdList.push(colIds.get(colName)!); -// } - -// /** 2. gather primary keys, per‑column indexes, uniques, sequences */ -// const pk: ColList = []; -// const indexes: RawIndexDefV9[] = []; -// const constraints: RawConstraintDefV9[] = []; -// const sequences: RawSequenceDefV9[] = []; - -// let scheduleAtCol: ColId | undefined; - -// for (const [name, builder] of Object.entries(def) as [ -// keyof Row & string, -// ColumnBuilder, -// ][]) { -// const meta: any = builder.__meta__; - -// /* primary key */ -// if (meta.primaryKey) pk.push(colIds.get(name)!); - -// /* implicit 1‑column indexes */ -// if (meta.index) { -// const algo = (meta.index ?? 'btree') as 'BTree' | 'Hash' | 'Direct'; -// const id = colIds.get(name)!; -// indexes.push( -// algo === 'Direct' -// ? { name: "TODO", accessorName: "TODO", algorithm: RawIndexAlgorithm.Direct(id) } -// : { name: "TODO", accessorName: "TODO", algorithm: { tag: algo, value: [id] } } -// ); -// } - -// /* uniqueness */ -// if (meta.unique) { -// constraints.push({ -// name: "TODO", -// data: { tag: 'Unique', value: { columns: [colIds.get(name)!] } }, -// }); -// } - -// /* auto increment */ -// if (meta.autoInc) { -// sequences.push({ -// name: "TODO", -// start: 0n, // TODO -// minValue: 0n, // TODO -// maxValue: 0n, // TODO -// column: colIds.get(name)!, -// increment: 1n, -// }); -// } - -// /* scheduleAt */ -// if (meta.scheduleAt) scheduleAtCol = colIds.get(name)!; -// } - -// /** 3. convert explicit multi‑column indexes coming from options.indexes */ -// for (const pending of (userIndexes ?? []) as PendingIndex< -// keyof Row & string -// >[]) { -// const converted: RawIndexDefV9 = { -// name: pending.name, -// accessorName: pending.accessor_name, -// algorithm: ((): RawIndexAlgorithm => { -// if (pending.algorithm.tag === 'Direct') -// return { -// tag: 'Direct', -// value: colIds.get(pending.algorithm.value.column)!, -// }; -// return { -// tag: pending.algorithm.tag, -// value: pending.algorithm.value.columns.map(c => colIds.get(c)!), -// }; -// })(), -// }; -// indexes.push(converted); -// } - -// /** 4. add the product type to the global Typespace */ -// const productTypeRef: AlgebraicTypeRef = MODULE_DEF.typespace.types.length; -// MODULE_DEF.typespace.types.push(row.__spacetime_type__); - -// /** 5. finalise table record */ -// const tableDef: RawTableDefV9 = { -// name, -// productTypeRef, -// primaryKey: pk, -// indexes, -// constraints, -// sequences, -// schedule: -// scheduled && scheduleAtCol !== undefined -// ? { -// name: "TODO", -// reducerName: scheduled, -// scheduledAtColumn: scheduleAtCol, -// } -// : undefined, -// tableType: { tag: 'User' }, -// tableAccess: { tag: isPublic ? 'Public' : 'Private' }, -// }; -// MODULE_DEF.tables.push(tableDef); - -// return { -// __table_name__: name as TableName, -// __row_type__: {} as Infer, -// __row_spacetime_type__: row.__spacetime_type__, -// } as TableHandle>; -// } - -// /***************************************************************** -// * schema() – Fixed to properly infer table names and row types -// *****************************************************************/ - -// /***************************************************************** -// * reducer() -// *****************************************************************/ -// type ParamsAsObject>> = { -// [K in keyof ParamDef]: Infer; -// }; - -// /***************************************************************** -// * procedure() -// * -// * Stored procedures are opaque to the DB engine itself, so we just -// * keep them out of `RawModuleDefV9` for now – you can forward‑declare -// * a companion `RawMiscModuleExportV9` type later if desired. -// *****************************************************************/ -// export function procedure< -// Name extends string, -// Params extends Record>, -// Ctx, -// R, -// >( -// _name: Name, -// _params: Params, -// _fn: (ctx: Ctx, payload: ParamsAsObject) => Promise | R -// ): void { -// /* nothing to push yet — left for your misc export section */ -// } - -// /***************************************************************** -// * internal: pushReducer() helper used by reducer() and lifecycle wrappers -// *****************************************************************/ -// function pushReducer< -// S, -// Name extends string = string, -// Params extends Record> = Record< -// string, -// ColumnBuilder -// >, -// >( -// name: Name, -// params: Params | ProductTypeColumnBuilder, -// lifecycle?: RawReducerDefV9['lifecycle'] -// ): void { -// // Allow either a product-type ColumnBuilder or a plain params object -// const paramsInternal: Params = -// (params as any).__is_product_type__ === true -// ? (params as ProductTypeColumnBuilder).__def__ -// : (params as Params); - -// const paramType = { -// elements: Object.entries(paramsInternal).map( -// ([n, c]) => -// ({ name: n, algebraicType: (c as ColumnBuilder).__spacetime_type__ }) -// ) -// }; - -// MODULE_DEF.reducers.push({ -// name, -// params: paramType, -// lifecycle, // <- lifecycle flag lands here -// }); -// } - -// /***************************************************************** -// * reducer() – leave behavior the same; delegate to pushReducer() -// *****************************************************************/ - - -// /***************************************************************** -// * Lifecycle reducers -// * - register with lifecycle: 'init' | 'on_connect' | 'on_disconnect' -// * - keep the same call shape you're already using -// *****************************************************************/ -// export function init< -// S extends Record = any, -// Params extends Record> = {}, -// >( -// name: 'init' = 'init', -// params: Params | ProductTypeColumnBuilder = {} as any, -// _fn?: (ctx: ReducerCtx, payload: ParamsAsObject) => void -// ): void { -// pushReducer(name, params, Lifecycle.Init); -// } - -// export function clientConnected< -// S extends Record = any, -// Params extends Record> = {}, -// >( -// name: 'on_connect' = 'on_connect', -// params: Params | ProductTypeColumnBuilder = {} as any, -// _fn?: (ctx: ReducerCtx, payload: ParamsAsObject) => void -// ): void { -// pushReducer(name, params, Lifecycle.OnConnect); -// } - -// export function clientDisconnected< -// S extends Record = any, -// Params extends Record> = {}, -// >( -// name: 'on_disconnect' = 'on_disconnect', -// params: Params | ProductTypeColumnBuilder = {} as any, -// _fn?: (ctx: ReducerCtx, payload: ParamsAsObject) => void -// ): void { -// pushReducer(name, params, Lifecycle.OnDisconnect); -// } - -// /***************************************************************** -// * Example usage with explicit interfaces for better navigation -// *****************************************************************/ -// const point = t.object({ -// x: t.f64(), -// y: t.f64(), -// }); -// type Point = Infer; - -// const user = { -// id: t.string().primaryKey(), -// name: t.string().index('btree'), -// email: t.string(), -// age: t.number(), -// }; -// type User = Infer; - -// const player = { -// id: t.u32().primaryKey().autoInc(), -// name: t.string().index('btree'), -// score: t.number(), -// level: t.number(), -// foo: t.number().unique(), -// bar: t.object({ -// x: t.f64(), -// y: t.f64(), -// }), -// baz: t.enum({ -// Foo: t.f64(), -// Bar: t.f64(), -// Baz: t.string(), -// }), -// }; - - -// const sendMessageSchedule = t.object({ -// scheduleId: t.u64().primaryKey(), -// scheduledAt: t.scheduleAt(), -// text: t.string(), -// }); - -// // Create the schema with named references -// const s = schema( -// table({ -// name: 'player', -// public: true, -// indexes: [ -// t.index({ name: 'my_index' }).btree({ columns: ['name', 'score'] }), -// ], -// }, player), -// table({ name: 'logged_out_user' }, user), -// table({ name: 'user' }, user), -// table({ -// name: 'send_message_schedule', -// scheduled: 'move_player', -// }, sendMessageSchedule) -// ); - -// // Export explicit type alias for the schema -// export type Schemar = InferSchema; - -// const foo = reducer('move_player', { user, point, player }, (ctx, { user, point, player }) => { -// ctx.db.send_message_schedule.insert({ -// scheduleId: 1, -// scheduledAt: ScheduleAt.Interval(234_000n), -// text: 'Move player' -// }); - -// ctx.db.player.insert(player); - -// if (player.baz.tag === 'Foo') { -// player.baz.value += 1; -// } else if (player.baz.tag === 'Bar') { -// player.baz.value += 2; -// } else if (player.baz.tag === 'Baz') { -// player.baz.value += '!'; -// } -// }); - -// const bar = reducer('foobar', {}, (ctx) => { -// bar(ctx, {}); -// }) - -// init('init', {}, (_ctx) => { - -// }) - -// // Result like Rust -// export type Result = -// | { ok: true; value: T } -// | { ok: false; error: E }; - -// // /* ───── generic index‑builder to be used in table options ───── */ -// // index(opts?: { -// // name?: IdxName; -// // }): { -// // btree(def: { -// // columns: Cols; -// // }): PendingIndex<(typeof def.columns)[number]>; -// // hash(def: { -// // columns: Cols; -// // }): PendingIndex<(typeof def.columns)[number]>; -// // direct(def: { column: Col }): PendingIndex; -// // } { -// // const common = { name: opts?.name }; -// // return { -// // btree(def: { columns: Cols }) { -// // return { -// // ...common, -// // algorithm: { -// // tag: 'BTree', -// // value: { columns: def.columns }, -// // }, -// // } as PendingIndex<(typeof def.columns)[number]>; -// // }, -// // hash(def: { columns: Cols }) { -// // return { -// // ...common, -// // algorithm: { -// // tag: 'Hash', -// // value: { columns: def.columns }, -// // }, -// // } as PendingIndex<(typeof def.columns)[number]>; -// // }, -// // direct(def: { column: Col }) { -// // return { -// // ...common, -// // algorithm: { -// // tag: 'Direct', -// // value: { column: def.column }, -// // }, -// // } as PendingIndex; -// // }, -// // }; -// // }, - -// // type TableOpts< -// // N extends string, -// // Def extends Record>, -// // Idx extends PendingIndex[] | undefined = undefined, -// // > = { -// // name: N; -// // public?: boolean; -// // indexes?: Idx; // declarative multi‑column indexes -// // scheduled?: string; // reducer name for cron‑like tables -// // }; - - -// // export function table< -// // const Name extends string, -// // Def extends Record>, -// // Row extends ProductTypeColumnBuilder, -// // Idx extends PendingIndex[] | undefined = undefined, -// // >(opts: TableOpts, row: Row): TableHandle, Name> { - - - -// // /** -// // * Creates a schema from table definitions -// // * @param handles - Array of table handles created by table() function -// // * @returns ColumnBuilder representing the complete database schema -// // * @example -// // * ```ts -// // * const s = schema( -// // * table({ name: 'users' }, userTable), -// // * table({ name: 'posts' }, postTable) -// // * ); -// // * ``` -// // */ -// // export function schema< -// // const H extends readonly TableHandle[] -// // >(...handles: H): ColumnBuilder> & { -// // /** @internal - for IDE navigation to schema variable */ -// // readonly __schema_definition__?: never; -// // }; - -// // /** -// // * Creates a schema from table definitions (array overload) -// // * @param handles - Array of table handles created by table() function -// // * @returns ColumnBuilder representing the complete database schema -// // */ -// // export function schema< -// // const H extends readonly TableHandle[] -// // >(handles: H): ColumnBuilder> & { -// // /** @internal - for IDE navigation to schema variable */ -// // readonly __schema_definition__?: never; -// // }; - -// // export function schema(...args: any[]): ColumnBuilder { -// // const handles = -// // (args.length === 1 && Array.isArray(args[0]) ? args[0] : args) as TableHandle[]; - -// // const productTy = AlgebraicType.Product({ -// // elements: handles.map(h => ({ -// // name: h.__table_name__, -// // algebraicType: h.__row_spacetime_type__, -// // })), -// // }); - -// // return col(productTy); -// // } - -// type UntypedTablesTuple = TableHandle[]; -// function schema(...tablesTuple: TablesTuple): Schema { -// return { -// tables: tablesTuple -// } -// } - -// type UntypedSchemaDef = { -// typespace: Typespace, -// tables: [RawTableDefV9], -// } - -// type Schema = { -// tables: Tables, -// } - -// type TableHandle = { -// readonly __table_name__: TableName; -// readonly __row_type__: Row; -// readonly __row_spacetime_type__: AlgebraicType; -// }; - -// type InferSchema = SchemaDef extends Schema ? Tables : never; - -// /** -// * Reducer context parametrized by the inferred Schema -// */ -// export type ReducerCtx = { -// db: DbView; -// }; - - -// type DbView = { -// [K in keyof SchemaDef]: Table> -// }; - - -// // schema provided -> ctx.db is precise -// export function reducer< -// S extends Record, -// Name extends string = string, -// Params extends Record> = Record< -// string, -// ColumnBuilder -// >, -// F = (ctx: ReducerCtx, payload: ParamsAsObject) => void, -// >(name: Name, params: Params | ProductTypeColumnBuilder, fn: F): F; - -// // no schema provided -> ctx.db is permissive -// export function reducer< -// Name extends string = string, -// Params extends Record> = Record< -// string, -// ColumnBuilder -// >, -// F = (ctx: ReducerCtx, payload: ParamsAsObject) => void, -// >(name: Name, params: Params | ProductTypeColumnBuilder, fn: F): F; - -// // single implementation (S defaults to any -> JS-like) -// export function reducer< -// S extends Record = any, -// Name extends string = string, -// Params extends Record> = Record< -// string, -// ColumnBuilder -// >, -// F = (ctx: ReducerCtx, payload: ParamsAsObject) => void, -// >(name: Name, params: Params | ProductTypeColumnBuilder, fn: F): F { -// pushReducer(name, params); -// return fn; -// } - - - - - - - -// // export type Infer = S extends ColumnBuilder ? JS : never; - -// // Create interfaces for each table to enable better navigation -// type TableHandleTupleToObject[]> = -// T extends readonly [TableHandle, ...infer Rest] -// ? Rest extends readonly TableHandle[] -// ? { [K in N1]: R1 } & TableHandleTupleToObject -// : { [K in N1]: R1 } -// : {}; - -// // Alternative approach - direct tuple iteration with interfaces -// type TupleToSchema[]> = TableHandleTupleToObject; - -// type TableNamesInSchemaDef = -// keyof SchemaDef & string; - -// type TableByName< -// SchemaDef extends UntypedSchemaDef, -// TableName extends TableNamesInSchemaDef, -// > = SchemaDef[TableName]; - -// type RowFromTable = -// TableDef["row"]; - -// /** -// * Reducer context parametrized by the inferred Schema -// */ -// type ReducerContext = { -// db: DbView; -// }; - -// type AnyTable = Table; -// type AnySchema = Record; - -// type Outer = { - -// } - -// type ReducerBuilder = { - -// } - -// type Local = {}; - -// /** -// * Table -// * -// * - Row: row shape -// * - UCV: unique-constraint violation error type (never if none) -// * - AIO: auto-increment overflow error type (never if none) -// */ -// export type Table = { -// /** Returns the number of rows in the TX state. */ -// count(): number; - -// /** Iterate over all rows in the TX state. Rust IteratorIterator → TS Iterable. */ -// iter(): IterableIterator; - -// /** Insert and return the inserted row (auto-increment fields filled). May throw on error. */ -// insert(row: Row): Row; - -// /** Like insert, but returns a Result instead of throwing. */ -// try_insert(row: Row): Result; - -// /** Delete a row equal to `row`. Returns true if something was deleted. */ -// delete(row: Row): boolean; -// }; - - -// type DbContext> = { -// db: DbView, -// }; From 3be7b0eb709c83c75c9b2a8a039c5c4c54761165 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 2 Sep 2025 18:52:24 -0400 Subject: [PATCH 4/8] Formatted --- .../src/server/type_builders.ts | 517 +++++++++++++----- .../src/server/type_util.ts | 3 +- crates/bindings-typescript/test/index.test.ts | 14 +- crates/bindings-typescript/vitest.config.ts | 8 +- 4 files changed, 384 insertions(+), 158 deletions(-) diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts index 67e8c522356..924222302d3 100644 --- a/crates/bindings-typescript/src/server/type_builders.ts +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -1,6 +1,12 @@ -import { AlgebraicType, ProductTypeElement, ScheduleAt, SumTypeVariant, type AlgebraicTypeVariants } from ".."; -import type __AlgebraicType from "../autogen/algebraic_type_type"; -import type { Prettify } from "./type_util"; +import { + AlgebraicType, + ProductTypeElement, + ScheduleAt, + SumTypeVariant, + type AlgebraicTypeVariants, +} from '..'; +import type __AlgebraicType from '../autogen/algebraic_type_type'; +import type { Prettify } from './type_util'; /** * A set of methods for building a column definition. Type builders extend this @@ -12,35 +18,59 @@ interface IntoColumnBuilder { * Specify the index type for this column * @param algorithm The index algorithm to use */ - index( + index< + M extends ColumnMetadata = DefaultMetadata, + N extends IndexTypes = 'btree', + >( algorithm?: N - ): ColumnBuilder & { indexType: N }>>; + ): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { indexType: N }> + >; /** * Specify this column as the primary key */ - primaryKey(): ColumnBuilder & { isPrimaryKey: true }>>; + primaryKey(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isPrimaryKey: true }> + >; /** * Specify this column as unique */ - unique(): ColumnBuilder & { isUnique: true }>>; + unique(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isUnique: true }> + >; /** * Specify this column as auto-incrementing */ - autoInc(): ColumnBuilder & { isAutoIncrement: true }>>; + autoInc(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isAutoIncrement: true }> + >; /** * Specify this column as a schedule-at field */ - scheduleAt(): ColumnBuilder & { isScheduleAt: true }>>; + scheduleAt(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isScheduleAt: true }> + >; } /** * Helper type to extract the TypeScript type from a TypeBuilder */ -type InferTypeOfTypeBuilder = T extends TypeBuilder ? U : never; +type InferTypeOfTypeBuilder = + T extends TypeBuilder ? U : never; /** * Helper type to extract the TypeScript type from a TypeBuilder @@ -50,7 +80,19 @@ export type Infer = InferTypeOfTypeBuilder; /** * Helper type to extract the type of a row from an object. */ -type InferTypeOfRow = T extends Record | TypeBuilder> ? { [K in keyof T]: T[K] extends ColumnBuilder ? V : T[K] extends TypeBuilder ? V : never } : never; +type InferTypeOfRow = + T extends Record< + string, + ColumnBuilder | TypeBuilder + > + ? { + [K in keyof T]: T[K] extends ColumnBuilder + ? V + : T[K] extends TypeBuilder + ? V + : never; + } + : never; /** * Type which represents a valid argument to the ProductColumnBuilder @@ -60,7 +102,10 @@ type ElementsObj = Record>; /** * Type which converts the elements of ElementsObj to a ProductType elements array */ -type ElementsArrayFromElementsObj = { name: keyof Obj & string; algebraicType: Obj[keyof Obj & string]["spacetimeType"] }[]; +type ElementsArrayFromElementsObj = { + name: keyof Obj & string; + algebraicType: Obj[keyof Obj & string]['spacetimeType']; +}[]; /** * A type which converts the elements of ElementsObj to a TypeScript object type. @@ -70,7 +115,7 @@ type ElementsArrayFromElementsObj = { name: keyof Obj & * e.g. { a: I32TypeBuilder, b: StringBuilder } -> { a: number, b: string } */ type TypeScriptTypeFromElementsObj = { - [K in keyof Elements]: InferTypeOfTypeBuilder + [K in keyof Elements]: InferTypeOfTypeBuilder; }; type VariantsObj = Record>; @@ -83,25 +128,30 @@ type VariantsObj = Record>; * e.g. { A: I32TypeBuilder, B: StringBuilder } -> { tag: "A", value: number } | { tag: "B", value: string } */ type TypeScriptTypeFromVariantsObj = { - [K in keyof Variants]: { tag: K; value: InferTypeOfTypeBuilder } + [K in keyof Variants]: { tag: K; value: InferTypeOfTypeBuilder }; }[keyof Variants]; /** * Type which converts the elements of VariantsObj to a SumType variants array */ -type VariantsArrayFromVariantsObj = { name: keyof Obj & string; algebraicType: Obj[keyof Obj & string]["spacetimeType"] }[]; +type VariantsArrayFromVariantsObj = { + name: keyof Obj & string; + algebraicType: Obj[keyof Obj & string]['spacetimeType']; +}[]; /** * A generic type builder that captures both the TypeScript type * and the corresponding `AlgebraicType`. */ -export class TypeBuilder implements IntoColumnBuilder { +export class TypeBuilder + implements IntoColumnBuilder +{ /** * The TypeScript phantom type. This is not stored at runtime, * but is visible to the compiler */ readonly type!: Type; - + /** * TypeScript phantom type representing the type of the particular * AlgebraicType stored in {@link algebraicType}. @@ -127,52 +177,95 @@ export class TypeBuilder implements I * Specify the index type for this column * @param algorithm The index algorithm to use */ - index( + index< + M extends ColumnMetadata = DefaultMetadata, + N extends IndexTypes = 'btree', + >( algorithm?: N - ): ColumnBuilder & { indexType: N }>> { - return new ColumnBuilder & { indexType: N }>>(this, { + ): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { indexType: N }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { indexType: N }> + >(this, { ...defaultMetadata, - indexType: algorithm + indexType: algorithm, }); } /** * Specify this column as the primary key */ - primaryKey(): ColumnBuilder & { isPrimaryKey: true }>> { - return new ColumnBuilder & { isPrimaryKey: true }>>(this, { + primaryKey(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isPrimaryKey: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isPrimaryKey: true }> + >(this, { ...defaultMetadata, - isPrimaryKey: true + isPrimaryKey: true, }); } /** * Specify this column as unique */ - unique(): ColumnBuilder & { isUnique: true }>> { - return new ColumnBuilder & { isUnique: true }>>(this, { + unique(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isUnique: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isUnique: true }> + >(this, { ...defaultMetadata, - isUnique: true + isUnique: true, }); } /** * Specify this column as auto-incrementing */ - autoInc(): ColumnBuilder & { isAutoIncrement: true }>> { - return new ColumnBuilder & { isAutoIncrement: true }>>(this, { + autoInc(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isAutoIncrement: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isAutoIncrement: true }> + >(this, { ...defaultMetadata, - isAutoIncrement: true + isAutoIncrement: true, }); } /** * Specify this column as a schedule-at field */ - scheduleAt(): ColumnBuilder & { isScheduleAt: true }>> { - return new ColumnBuilder & { isScheduleAt: true }>>(this, { + scheduleAt(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isScheduleAt: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isScheduleAt: true }> + >(this, { ...defaultMetadata, - isScheduleAt: true + isScheduleAt: true, }); } } @@ -197,22 +290,34 @@ export class U64Builder extends TypeBuilder { super(AlgebraicType.U64); } } -export class U128Builder extends TypeBuilder { +export class U128Builder extends TypeBuilder< + bigint, + AlgebraicTypeVariants.U128 +> { constructor() { super(AlgebraicType.U128); } } -export class U256Builder extends TypeBuilder { +export class U256Builder extends TypeBuilder< + bigint, + AlgebraicTypeVariants.U256 +> { constructor() { super(AlgebraicType.U256); } } -export class I128Builder extends TypeBuilder { +export class I128Builder extends TypeBuilder< + bigint, + AlgebraicTypeVariants.I128 +> { constructor() { super(AlgebraicType.I128); } } -export class I256Builder extends TypeBuilder { +export class I256Builder extends TypeBuilder< + bigint, + AlgebraicTypeVariants.I256 +> { constructor() { super(AlgebraicType.I256); } @@ -227,12 +332,18 @@ export class F64Builder extends TypeBuilder { super(AlgebraicType.F64); } } -export class BoolBuilder extends TypeBuilder { +export class BoolBuilder extends TypeBuilder< + boolean, + AlgebraicTypeVariants.Bool +> { constructor() { super(AlgebraicType.Bool); } } -export class StringBuilder extends TypeBuilder { +export class StringBuilder extends TypeBuilder< + string, + AlgebraicTypeVariants.String +> { constructor() { super(AlgebraicType.String); } @@ -257,7 +368,12 @@ export class I64Builder extends TypeBuilder { super(AlgebraicType.I64); } } -export class ArrayBuilder> extends TypeBuilder, { tag: "Array", value: Element["spacetimeType"] }> { +export class ArrayBuilder< + Element extends TypeBuilder, +> extends TypeBuilder< + Array, + { tag: 'Array'; value: Element['spacetimeType'] } +> { /** * The phantom element type of the array for TypeScript */ @@ -267,7 +383,13 @@ export class ArrayBuilder> extends TypeBui super(AlgebraicType.Array(element.algebraicType)); } } -export class ProductBuilder extends TypeBuilder, { tag: "Product", value: { elements: ElementsArrayFromElementsObj } }> { +export class ProductBuilder extends TypeBuilder< + TypeScriptTypeFromElementsObj, + { + tag: 'Product'; + value: { elements: ElementsArrayFromElementsObj }; + } +> { /** * The phantom element types of the product for TypeScript */ @@ -275,54 +397,101 @@ export class ProductBuilder extends TypeBuilder(obj: Obj) { - return Object.entries(obj).map(([name, { algebraicType }]) => ({ name, algebraicType })); + return Object.entries(obj).map(([name, { algebraicType }]) => ({ + name, + algebraicType, + })); } - super(AlgebraicType.Product({ - elements: elementsArrayFromElementsObj(elements) - })); + super( + AlgebraicType.Product({ + elements: elementsArrayFromElementsObj(elements), + }) + ); } } -export class SumBuilder extends TypeBuilder, { tag: "Sum", value: { variants: VariantsArrayFromVariantsObj } }> { +export class SumBuilder extends TypeBuilder< + TypeScriptTypeFromVariantsObj, + { tag: 'Sum'; value: { variants: VariantsArrayFromVariantsObj } } +> { /** * The phantom variant types of the sum for TypeScript */ readonly variants!: Variants; constructor(variants: Variants) { - function variantsArrayFromVariantsObj(variants: Variants): SumTypeVariant[] { - return Object.entries(variants).map(([name, { algebraicType }]) => ({ name, algebraicType })); + function variantsArrayFromVariantsObj( + variants: Variants + ): SumTypeVariant[] { + return Object.entries(variants).map(([name, { algebraicType }]) => ({ + name, + algebraicType, + })); } - super(AlgebraicType.Sum({ - variants: variantsArrayFromVariantsObj(variants) - })); + super( + AlgebraicType.Sum({ + variants: variantsArrayFromVariantsObj(variants), + }) + ); } } -export interface U8ColumnBuilder extends ColumnBuilder { } -export interface U16ColumnBuilder extends ColumnBuilder { } -export interface U32ColumnBuilder extends ColumnBuilder { } -export interface U64ColumnBuilder extends ColumnBuilder { } -export interface U128ColumnBuilder extends ColumnBuilder { } -export interface U256ColumnBuilder extends ColumnBuilder { } -export interface I8ColumnBuilder extends ColumnBuilder { } -export interface I16ColumnBuilder extends ColumnBuilder { } -export interface I32ColumnBuilder extends ColumnBuilder { } -export interface I64ColumnBuilder extends ColumnBuilder { } -export interface I128ColumnBuilder extends ColumnBuilder { } -export interface I256ColumnBuilder extends ColumnBuilder { } -export interface F32ColumnBuilder extends ColumnBuilder { } -export interface F64ColumnBuilder extends ColumnBuilder { } -export interface BoolColumnBuilder extends ColumnBuilder { } -export interface StringColumnBuilder extends ColumnBuilder { } -export interface ArrayColumnBuilder> extends ColumnBuilder, { tag: "Array", value: Element["spacetimeType"] }> { } -export interface ProductColumnBuilder> extends ColumnBuilder<{ [K in Elements[number]['name']]: any }, { tag: "Product", value: { elements: Elements } }> { } -export interface SumColumnBuilder> extends ColumnBuilder<{ [K in Variants[number]['name']]: { tag: K; value: any } }[Variants[number]['name']], { tag: "Sum", value: { variants: Variants } }> { } +export interface U8ColumnBuilder + extends ColumnBuilder {} +export interface U16ColumnBuilder + extends ColumnBuilder {} +export interface U32ColumnBuilder + extends ColumnBuilder {} +export interface U64ColumnBuilder + extends ColumnBuilder {} +export interface U128ColumnBuilder + extends ColumnBuilder {} +export interface U256ColumnBuilder + extends ColumnBuilder {} +export interface I8ColumnBuilder + extends ColumnBuilder {} +export interface I16ColumnBuilder + extends ColumnBuilder {} +export interface I32ColumnBuilder + extends ColumnBuilder {} +export interface I64ColumnBuilder + extends ColumnBuilder {} +export interface I128ColumnBuilder + extends ColumnBuilder {} +export interface I256ColumnBuilder + extends ColumnBuilder {} +export interface F32ColumnBuilder + extends ColumnBuilder {} +export interface F64ColumnBuilder + extends ColumnBuilder {} +export interface BoolColumnBuilder + extends ColumnBuilder {} +export interface StringColumnBuilder + extends ColumnBuilder {} +export interface ArrayColumnBuilder> + extends ColumnBuilder< + Array, + { tag: 'Array'; value: Element['spacetimeType'] } + > {} +export interface ProductColumnBuilder< + Elements extends Array<{ name: string; algebraicType: AlgebraicType }>, +> extends ColumnBuilder< + { [K in Elements[number]['name']]: any }, + { tag: 'Product'; value: { elements: Elements } } + > {} +export interface SumColumnBuilder< + Variants extends Array<{ name: string; algebraicType: AlgebraicType }>, +> extends ColumnBuilder< + { + [K in Variants[number]['name']]: { tag: K; value: any }; + }[Variants[number]['name']], + { tag: 'Sum'; value: { variants: Variants } } + > {} /** * The type of index types that can be applied to a column. * `undefined` is the default */ -type IndexTypes = "btree" | "hash" | undefined; +type IndexTypes = 'btree' | 'hash' | undefined; /** * Metadata describing column constraints and index type @@ -354,7 +523,7 @@ const defaultMetadata: DefaultMetadata = { isUnique: false, isAutoIncrement: false, isScheduleAt: false, - indexType: undefined + indexType: undefined, }; /** @@ -367,12 +536,16 @@ const defaultMetadata: DefaultMetadata = { export class ColumnBuilder< Type, SpacetimeType extends AlgebraicType, - M extends ColumnMetadata = DefaultMetadata -> implements IntoColumnBuilder { + M extends ColumnMetadata = DefaultMetadata, +> implements IntoColumnBuilder +{ typeBuilder: TypeBuilder; columnMetadata: ColumnMetadata; - constructor(typeBuilder: TypeBuilder, metadata?: ColumnMetadata) { + constructor( + typeBuilder: TypeBuilder, + metadata?: ColumnMetadata + ) { this.typeBuilder = typeBuilder; this.columnMetadata = defaultMetadata; } @@ -381,68 +554,93 @@ export class ColumnBuilder< * Specify the index type for this column * @param algorithm The index algorithm to use */ - index( + index( algorithm?: N - ): ColumnBuilder & { indexType: N }>> { - return new ColumnBuilder & { indexType: N }>>( - this.typeBuilder, - { - ...this.columnMetadata, - indexType: algorithm - } - ); + ): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { indexType: N }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { indexType: N }> + >(this.typeBuilder, { + ...this.columnMetadata, + indexType: algorithm, + }); } /** * Specify this column as the primary key */ - primaryKey(): ColumnBuilder & { isPrimaryKey: true }>> { - return new ColumnBuilder & { isPrimaryKey: true }>>( - this.typeBuilder, - { - ...this.columnMetadata, - isPrimaryKey: true - } - ); + primaryKey(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isPrimaryKey: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isPrimaryKey: true }> + >(this.typeBuilder, { + ...this.columnMetadata, + isPrimaryKey: true, + }); } /** * Specify this column as unique */ - unique(): ColumnBuilder & { isUnique: true }>> { - return new ColumnBuilder & { isUnique: true }>>( - this.typeBuilder, - { - ...this.columnMetadata, - isUnique: true - } - ); + unique(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isUnique: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isUnique: true }> + >(this.typeBuilder, { + ...this.columnMetadata, + isUnique: true, + }); } /** * Specify this column as auto-incrementing */ - autoInc(): ColumnBuilder & { isAutoIncrement: true }>> { - return new ColumnBuilder & { isAutoIncrement: true }>>( - this.typeBuilder, - { - ...this.columnMetadata, - isAutoIncrement: true - } - ); + autoInc(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isAutoIncrement: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isAutoIncrement: true }> + >(this.typeBuilder, { + ...this.columnMetadata, + isAutoIncrement: true, + }); } /** * Specify this column as a schedule-at field */ - scheduleAt(): ColumnBuilder & { isScheduleAt: true }>> { - return new ColumnBuilder & { isScheduleAt: true }>>( - this.typeBuilder, - { - ...this.columnMetadata, - isScheduleAt: true - } - ); + scheduleAt(): ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isScheduleAt: true }> + > { + return new ColumnBuilder< + Type, + SpacetimeType, + Prettify & { isScheduleAt: true }> + >(this.typeBuilder, { + ...this.columnMetadata, + isScheduleAt: true, + }); } } @@ -591,9 +789,7 @@ const t = { * values must be {@link TypeBuilder}s. * @returns A new ObjectBuilder instance */ - object( - obj: Obj - ): ProductBuilder { + object(obj: Obj): ProductBuilder { return new ProductBuilder(obj); }, @@ -603,7 +799,9 @@ const t = { * @param element The element type of the array, which must be a `TypeBuilder`. * @returns A new ArrayBuilder instance */ - array>(e: Element): ArrayBuilder { + array>( + e: Element + ): ArrayBuilder { return new ArrayBuilder(e); }, @@ -614,9 +812,7 @@ const t = { * types must be `TypeBuilder`s. * @returns A new EnumBuilder instance */ - enum( - obj: Obj - ): SumBuilder { + enum(obj: Obj): SumBuilder { return new SumBuilder(obj); }, @@ -624,12 +820,23 @@ const t = { * This is a special helper function for conveniently creating {@link ScheduleAt} type columns. * @returns A new ColumnBuilder instance with the {@link ScheduleAt} type. */ - scheduleAt: (): ColumnBuilder, Omit & { isScheduleAt: true }> => { - return new ColumnBuilder, Omit & { isScheduleAt: true }>( - new TypeBuilder>(ScheduleAt.getAlgebraicType()), + scheduleAt: (): ColumnBuilder< + ScheduleAt, + ReturnType, + Omit & { isScheduleAt: true } + > => { + return new ColumnBuilder< + ScheduleAt, + ReturnType, + Omit & { isScheduleAt: true } + >( + new TypeBuilder< + ScheduleAt, + ReturnType + >(ScheduleAt.getAlgebraicType()), { ...defaultMetadata, - isScheduleAt: true + isScheduleAt: true, } ); }, @@ -638,32 +845,32 @@ export default t; // @typescript-eslint/no-unused-vars namespace tests { - type MustBeNever = [T] extends [never] - ? true - : ["Error: Type must be never", T]; + type MustBeNever = [T] extends [never] + ? true + : ['Error: Type must be never', T]; // Test type inference on a row // i.e. a Record type const row = { foo: t.string(), bar: t.i32().primaryKey(), - idx: t.i64().index("btree").unique() + idx: t.i64().index('btree').unique(), }; type Row = InferTypeOfRow; const _row: Row = { - foo: "hello", + foo: 'hello', bar: 42, - idx: 100n + idx: 100n, }; // Test that a row must not allow non-TypeBuilder or ColumnBuilder values const row2 = { foo: { // bar is not a TypeBuilder or ColumnBuilder, so this should fail - bar: t.string() + bar: t.string(), }, bar: t.i32().primaryKey(), - idx: t.i64().index("btree").unique() + idx: t.i64().index('btree').unique(), }; type Row2 = InferTypeOfRow; type _ = MustBeNever; @@ -673,35 +880,55 @@ namespace tests { x: t.i32(), y: t.f64(), z: t.object({ - foo: t.string() - }) + foo: t.string(), + }), }); type Point = InferTypeOfTypeBuilder; const _point: Point = { x: 1.0, y: 2.0, z: { - foo: "bar" - } + foo: 'bar', + }, }; // Test type inference on an enum const e = t.enum({ A: t.string(), - B: t.number() + B: t.number(), }); type E = InferTypeOfTypeBuilder; - const _e: E = { tag: "A", value: "hello" }; - const _e2: E = { tag: "B", value: 42 }; + const _e: E = { tag: 'A', value: 'hello' }; + const _e2: E = { tag: 'B', value: 42 }; // Test that the type of a row includes the correct ColumnBuilder types const _row3: { foo: TypeBuilder; - bar: ColumnBuilder; - idx: ColumnBuilder; + bar: ColumnBuilder< + number, + AlgebraicTypeVariants.I32, + { + isPrimaryKey: true; + isUnique: false; + isAutoIncrement: false; + isScheduleAt: false; + indexType: undefined; + } + >; + idx: ColumnBuilder< + bigint, + AlgebraicTypeVariants.I64, + { + isPrimaryKey: false; + isUnique: true; + isAutoIncrement: false; + isScheduleAt: false; + indexType: 'btree'; + } + >; } = { foo: t.string(), bar: t.i32().primaryKey(), - idx: t.i64().index("btree").unique() + idx: t.i64().index('btree').unique(), }; } diff --git a/crates/bindings-typescript/src/server/type_util.ts b/crates/bindings-typescript/src/server/type_util.ts index cf6518d4a51..2411f05d03f 100644 --- a/crates/bindings-typescript/src/server/type_util.ts +++ b/crates/bindings-typescript/src/server/type_util.ts @@ -1,5 +1,4 @@ - /** * Utility to make TS show cleaner types by flattening intersections. */ -export type Prettify = { [K in keyof T]: T[K] } & {}; \ No newline at end of file +export type Prettify = { [K in keyof T]: T[K] } & {}; diff --git a/crates/bindings-typescript/test/index.test.ts b/crates/bindings-typescript/test/index.test.ts index 29c41a9381d..beeec2c057a 100644 --- a/crates/bindings-typescript/test/index.test.ts +++ b/crates/bindings-typescript/test/index.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect } from 'vitest'; import { AlgebraicType, t } from '../src/index'; describe('TypeBuilder', () => { @@ -6,7 +6,7 @@ describe('TypeBuilder', () => { const point = t.object({ x: t.f64(), y: t.f64(), - z: t.f64() + z: t.f64(), }); expect(point.algebraicType).toEqual({ tag: 'Product', @@ -14,9 +14,9 @@ describe('TypeBuilder', () => { elements: [ { name: 'x', algebraicType: AlgebraicType.F64 }, { name: 'y', algebraicType: AlgebraicType.F64 }, - { name: 'z', algebraicType: AlgebraicType.F64 } - ] - } + { name: 'z', algebraicType: AlgebraicType.F64 }, + ], + }, }); - }) -}) + }); +}); diff --git a/crates/bindings-typescript/vitest.config.ts b/crates/bindings-typescript/vitest.config.ts index 5147cfd5555..6e6d96637b7 100644 --- a/crates/bindings-typescript/vitest.config.ts +++ b/crates/bindings-typescript/vitest.config.ts @@ -1,9 +1,9 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { include: ['test/**/*.test.ts'], globals: true, - environment: 'node' - } -}) \ No newline at end of file + environment: 'node', + }, +}); From 1358137f81d410dc7d9856b1714de7aeedfedf03 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Thu, 4 Sep 2025 08:55:43 -0400 Subject: [PATCH 5/8] Fixed lint errors --- .../src/server/type_builders.ts | 287 +++++++++--------- 1 file changed, 144 insertions(+), 143 deletions(-) diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts index 924222302d3..8d8166960aa 100644 --- a/crates/bindings-typescript/src/server/type_builders.ts +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -1,11 +1,9 @@ import { AlgebraicType, - ProductTypeElement, ScheduleAt, SumTypeVariant, type AlgebraicTypeVariants, } from '..'; -import type __AlgebraicType from '../autogen/algebraic_type_type'; import type { Prettify } from './type_util'; /** @@ -81,10 +79,7 @@ export type Infer = InferTypeOfTypeBuilder; * Helper type to extract the type of a row from an object. */ type InferTypeOfRow = - T extends Record< - string, - ColumnBuilder | TypeBuilder - > + T extends Record | TypeBuilder> ? { [K in keyof T]: T[K] extends ColumnBuilder ? V @@ -435,57 +430,64 @@ export class SumBuilder extends TypeBuilder< } } -export interface U8ColumnBuilder - extends ColumnBuilder {} -export interface U16ColumnBuilder - extends ColumnBuilder {} -export interface U32ColumnBuilder - extends ColumnBuilder {} -export interface U64ColumnBuilder - extends ColumnBuilder {} -export interface U128ColumnBuilder - extends ColumnBuilder {} -export interface U256ColumnBuilder - extends ColumnBuilder {} -export interface I8ColumnBuilder - extends ColumnBuilder {} -export interface I16ColumnBuilder - extends ColumnBuilder {} -export interface I32ColumnBuilder - extends ColumnBuilder {} -export interface I64ColumnBuilder - extends ColumnBuilder {} -export interface I128ColumnBuilder - extends ColumnBuilder {} -export interface I256ColumnBuilder - extends ColumnBuilder {} -export interface F32ColumnBuilder - extends ColumnBuilder {} -export interface F64ColumnBuilder - extends ColumnBuilder {} -export interface BoolColumnBuilder - extends ColumnBuilder {} -export interface StringColumnBuilder - extends ColumnBuilder {} -export interface ArrayColumnBuilder> - extends ColumnBuilder< - Array, - { tag: 'Array'; value: Element['spacetimeType'] } - > {} -export interface ProductColumnBuilder< +export type U8ColumnBuilder = + ColumnBuilder; +export type U16ColumnBuilder = + ColumnBuilder; +export type U32ColumnBuilder = + ColumnBuilder; +export type U64ColumnBuilder = + ColumnBuilder; +export type U128ColumnBuilder = + ColumnBuilder; +export type U256ColumnBuilder = + ColumnBuilder; +export type I8ColumnBuilder = + ColumnBuilder; +export type I16ColumnBuilder = + ColumnBuilder; +export type I32ColumnBuilder = + ColumnBuilder; +export type I64ColumnBuilder = + ColumnBuilder; +export type I128ColumnBuilder = + ColumnBuilder; +export type I256ColumnBuilder = + ColumnBuilder; +export type F32ColumnBuilder = + ColumnBuilder; +export type F64ColumnBuilder = + ColumnBuilder; +export type BoolColumnBuilder = + ColumnBuilder; +export type StringColumnBuilder = + ColumnBuilder; +export type ArrayColumnBuilder< + Element extends TypeBuilder, + M extends ColumnMetadata = DefaultMetadata, +> = ColumnBuilder< + Array, + { tag: 'Array'; value: Element['spacetimeType'] }, + M +>; +export type ProductColumnBuilder< Elements extends Array<{ name: string; algebraicType: AlgebraicType }>, -> extends ColumnBuilder< - { [K in Elements[number]['name']]: any }, - { tag: 'Product'; value: { elements: Elements } } - > {} -export interface SumColumnBuilder< + M extends ColumnMetadata = DefaultMetadata, +> = ColumnBuilder< + { [K in Elements[number]['name']]: any }, + { tag: 'Product'; value: { elements: Elements } }, + M +>; +export type SumColumnBuilder< Variants extends Array<{ name: string; algebraicType: AlgebraicType }>, -> extends ColumnBuilder< - { - [K in Variants[number]['name']]: { tag: K; value: any }; - }[Variants[number]['name']], - { tag: 'Sum'; value: { variants: Variants } } - > {} + M extends ColumnMetadata = DefaultMetadata, +> = ColumnBuilder< + { + [K in Variants[number]['name']]: { tag: K; value: any }; + }[Variants[number]['name']], + { tag: 'Sum'; value: { variants: Variants } }, + M +>; /** * The type of index types that can be applied to a column. @@ -547,7 +549,7 @@ export class ColumnBuilder< metadata?: ColumnMetadata ) { this.typeBuilder = typeBuilder; - this.columnMetadata = defaultMetadata; + this.columnMetadata = metadata ?? defaultMetadata; } /** @@ -843,92 +845,91 @@ const t = { } as const; export default t; -// @typescript-eslint/no-unused-vars -namespace tests { - type MustBeNever = [T] extends [never] - ? true - : ['Error: Type must be never', T]; +type MustBeNever = [T] extends [never] + ? true + : ['Error: Type must be never', T]; + +// Test type inference on a row +// i.e. a Record type +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const row = { + foo: t.string(), + bar: t.i32().primaryKey(), + idx: t.i64().index('btree').unique(), +}; +type Row = InferTypeOfRow; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _row: Row = { + foo: 'hello', + bar: 42, + idx: 100n, +}; - // Test type inference on a row - // i.e. a Record type - const row = { - foo: t.string(), - bar: t.i32().primaryKey(), - idx: t.i64().index('btree').unique(), - }; - type Row = InferTypeOfRow; - const _row: Row = { - foo: 'hello', - bar: 42, - idx: 100n, - }; - - // Test that a row must not allow non-TypeBuilder or ColumnBuilder values - const row2 = { - foo: { - // bar is not a TypeBuilder or ColumnBuilder, so this should fail - bar: t.string(), - }, - bar: t.i32().primaryKey(), - idx: t.i64().index('btree').unique(), - }; - type Row2 = InferTypeOfRow; - type _ = MustBeNever; - - // Test type inference on a type with a nested object - const point = t.object({ - x: t.i32(), - y: t.f64(), - z: t.object({ - foo: t.string(), - }), - }); - type Point = InferTypeOfTypeBuilder; - const _point: Point = { - x: 1.0, - y: 2.0, - z: { - foo: 'bar', - }, - }; - - // Test type inference on an enum - const e = t.enum({ - A: t.string(), - B: t.number(), - }); - type E = InferTypeOfTypeBuilder; - const _e: E = { tag: 'A', value: 'hello' }; - const _e2: E = { tag: 'B', value: 42 }; - - // Test that the type of a row includes the correct ColumnBuilder types - const _row3: { - foo: TypeBuilder; - bar: ColumnBuilder< - number, - AlgebraicTypeVariants.I32, - { - isPrimaryKey: true; - isUnique: false; - isAutoIncrement: false; - isScheduleAt: false; - indexType: undefined; - } - >; - idx: ColumnBuilder< - bigint, - AlgebraicTypeVariants.I64, - { - isPrimaryKey: false; - isUnique: true; - isAutoIncrement: false; - isScheduleAt: false; - indexType: 'btree'; - } - >; - } = { +// Test that a row must not allow non-TypeBuilder or ColumnBuilder values +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const row2 = { + foo: { + // bar is not a TypeBuilder or ColumnBuilder, so this should fail + bar: t.string(), + }, + bar: t.i32().primaryKey(), + idx: t.i64().index('btree').unique(), +}; +type Row2 = InferTypeOfRow; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type _ = MustBeNever; + +// Test type inference on a type with a nested object +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const point = t.object({ + x: t.i32(), + y: t.f64(), + z: t.object({ foo: t.string(), - bar: t.i32().primaryKey(), - idx: t.i64().index('btree').unique(), - }; -} + }), +}); +type Point = InferTypeOfTypeBuilder; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _point: Point = { + x: 1.0, + y: 2.0, + z: { + foo: 'bar', + }, +}; + +// Test type inference on an enum +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const e = t.enum({ + A: t.string(), + B: t.number(), +}); +type E = InferTypeOfTypeBuilder; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _e: E = { tag: 'A', value: 'hello' }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _e2: E = { tag: 'B', value: 42 }; + +// Test that the type of a row includes the correct ColumnBuilder types +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _row3: { + foo: TypeBuilder; + bar: I32ColumnBuilder<{ + isPrimaryKey: true; + isUnique: false; + isAutoIncrement: false; + isScheduleAt: false; + indexType: undefined; + }>; + idx: I64ColumnBuilder<{ + isPrimaryKey: false; + isUnique: true; + isAutoIncrement: false; + isScheduleAt: false; + indexType: 'btree'; + }>; +} = { + foo: t.string(), + bar: t.i32().primaryKey(), + idx: t.i64().index('btree').unique(), +}; From 41fed57cbbdc1aca052f20aea1e420a3cc2abd0b Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Thu, 4 Sep 2025 15:21:41 -0400 Subject: [PATCH 6/8] fixed errors with running tests and generating --- .../src/server/type_builders.test-d.ts | 91 ++++++++++++++++++ .../src/server/type_builders.ts | 95 +------------------ 2 files changed, 94 insertions(+), 92 deletions(-) create mode 100644 crates/bindings-typescript/src/server/type_builders.test-d.ts diff --git a/crates/bindings-typescript/src/server/type_builders.test-d.ts b/crates/bindings-typescript/src/server/type_builders.test-d.ts new file mode 100644 index 00000000000..b637a3761d6 --- /dev/null +++ b/crates/bindings-typescript/src/server/type_builders.test-d.ts @@ -0,0 +1,91 @@ +import { t, type AlgebraicTypeVariants } from ".."; +import type { I32ColumnBuilder, I64ColumnBuilder, InferTypeOfRow, InferTypeOfTypeBuilder, TypeBuilder } from "./type_builders"; + +type MustBeNever = [T] extends [never] + ? true + : ['Error: Type must be never', T]; + +// Test type inference on a row +// i.e. a Record type +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const row = { + foo: t.string(), + bar: t.i32().primaryKey(), + idx: t.i64().index('btree').unique(), +}; +type Row = InferTypeOfRow; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _row: Row = { + foo: 'hello', + bar: 42, + idx: 100n, +}; + +// Test that a row must not allow non-TypeBuilder or ColumnBuilder values +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const row2 = { + foo: { + // bar is not a TypeBuilder or ColumnBuilder, so this should fail + bar: t.string(), + }, + bar: t.i32().primaryKey(), + idx: t.i64().index('btree').unique(), +}; +type Row2 = InferTypeOfRow; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type _ = MustBeNever; + +// Test type inference on a type with a nested object +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const point = t.object({ + x: t.i32(), + y: t.f64(), + z: t.object({ + foo: t.string(), + }), +}); +type Point = InferTypeOfTypeBuilder; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _point: Point = { + x: 1.0, + y: 2.0, + z: { + foo: 'bar', + }, +}; + +// Test type inference on an enum +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const e = t.enum({ + A: t.string(), + B: t.number(), +}); +type E = InferTypeOfTypeBuilder; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _e: E = { tag: 'A', value: 'hello' }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _e2: E = { tag: 'B', value: 42 }; + +// Test that the type of a row includes the correct ColumnBuilder types +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _row3: { + foo: TypeBuilder; + bar: I32ColumnBuilder<{ + isPrimaryKey: true; + isUnique: false; + isAutoIncrement: false; + isScheduleAt: false; + indexType: undefined; + }>; + idx: I64ColumnBuilder<{ + isPrimaryKey: false; + isUnique: true; + isAutoIncrement: false; + isScheduleAt: false; + indexType: 'btree'; + }>; +} = { + foo: t.string(), + bar: t.i32().primaryKey(), + idx: t.i64().index('btree').unique(), +}; diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts index 8d8166960aa..9a0fbfd415d 100644 --- a/crates/bindings-typescript/src/server/type_builders.ts +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -67,7 +67,7 @@ interface IntoColumnBuilder { /** * Helper type to extract the TypeScript type from a TypeBuilder */ -type InferTypeOfTypeBuilder = +export type InferTypeOfTypeBuilder = T extends TypeBuilder ? U : never; /** @@ -78,7 +78,7 @@ export type Infer = InferTypeOfTypeBuilder; /** * Helper type to extract the type of a row from an object. */ -type InferTypeOfRow = +export type InferTypeOfRow = T extends Record | TypeBuilder> ? { [K in keyof T]: T[K] extends ColumnBuilder @@ -843,93 +843,4 @@ const t = { ); }, } as const; -export default t; - -type MustBeNever = [T] extends [never] - ? true - : ['Error: Type must be never', T]; - -// Test type inference on a row -// i.e. a Record type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const row = { - foo: t.string(), - bar: t.i32().primaryKey(), - idx: t.i64().index('btree').unique(), -}; -type Row = InferTypeOfRow; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _row: Row = { - foo: 'hello', - bar: 42, - idx: 100n, -}; - -// Test that a row must not allow non-TypeBuilder or ColumnBuilder values -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const row2 = { - foo: { - // bar is not a TypeBuilder or ColumnBuilder, so this should fail - bar: t.string(), - }, - bar: t.i32().primaryKey(), - idx: t.i64().index('btree').unique(), -}; -type Row2 = InferTypeOfRow; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type _ = MustBeNever; - -// Test type inference on a type with a nested object -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const point = t.object({ - x: t.i32(), - y: t.f64(), - z: t.object({ - foo: t.string(), - }), -}); -type Point = InferTypeOfTypeBuilder; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _point: Point = { - x: 1.0, - y: 2.0, - z: { - foo: 'bar', - }, -}; - -// Test type inference on an enum -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const e = t.enum({ - A: t.string(), - B: t.number(), -}); -type E = InferTypeOfTypeBuilder; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _e: E = { tag: 'A', value: 'hello' }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _e2: E = { tag: 'B', value: 42 }; - -// Test that the type of a row includes the correct ColumnBuilder types -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _row3: { - foo: TypeBuilder; - bar: I32ColumnBuilder<{ - isPrimaryKey: true; - isUnique: false; - isAutoIncrement: false; - isScheduleAt: false; - indexType: undefined; - }>; - idx: I64ColumnBuilder<{ - isPrimaryKey: false; - isUnique: true; - isAutoIncrement: false; - isScheduleAt: false; - indexType: 'btree'; - }>; -} = { - foo: t.string(), - bar: t.i32().primaryKey(), - idx: t.i64().index('btree').unique(), -}; +export default t; \ No newline at end of file From 9f9b9aa5f9f858da5c1ddfea4395503b2c98115f Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Fri, 5 Sep 2025 12:51:55 -0400 Subject: [PATCH 7/8] Added some testing --- .../src/server/type_builders.test-d.ts | 10 +- .../src/server/type_builders.ts | 35 ++---- crates/bindings-typescript/test/index.test.ts | 111 ++++++++++++++++++ 3 files changed, 126 insertions(+), 30 deletions(-) diff --git a/crates/bindings-typescript/src/server/type_builders.test-d.ts b/crates/bindings-typescript/src/server/type_builders.test-d.ts index b637a3761d6..d3b9ab5f1ac 100644 --- a/crates/bindings-typescript/src/server/type_builders.test-d.ts +++ b/crates/bindings-typescript/src/server/type_builders.test-d.ts @@ -1,5 +1,11 @@ -import { t, type AlgebraicTypeVariants } from ".."; -import type { I32ColumnBuilder, I64ColumnBuilder, InferTypeOfRow, InferTypeOfTypeBuilder, TypeBuilder } from "./type_builders"; +import { t, type AlgebraicTypeVariants } from '..'; +import type { + I32ColumnBuilder, + I64ColumnBuilder, + InferTypeOfRow, + InferTypeOfTypeBuilder, + TypeBuilder, +} from './type_builders'; type MustBeNever = [T] extends [never] ? true diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts index 9a0fbfd415d..ecdb618875c 100644 --- a/crates/bindings-typescript/src/server/type_builders.ts +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -10,6 +10,12 @@ import type { Prettify } from './type_util'; * A set of methods for building a column definition. Type builders extend this * interface so that they can be converted to column builders by calling * one of the methods that returns a column builder. + * + * Notably, `scheduleAt` is not part of this interface even though it is part + * of the metadata for a column because it specifies both the type of the + * column and the metadata as well. Therefore, it should not be possible to + * convert a normal type into a `ScheduleAt` column. You should instead use + * {@link t.scheduleAt} to create a column with the `ScheduleAt` type directly. */ interface IntoColumnBuilder { /** @@ -53,15 +59,6 @@ interface IntoColumnBuilder { SpacetimeType, Prettify & { isAutoIncrement: true }> >; - - /** - * Specify this column as a schedule-at field - */ - scheduleAt(): ColumnBuilder< - Type, - SpacetimeType, - Prettify & { isScheduleAt: true }> - >; } /** @@ -626,24 +623,6 @@ export class ColumnBuilder< isAutoIncrement: true, }); } - - /** - * Specify this column as a schedule-at field - */ - scheduleAt(): ColumnBuilder< - Type, - SpacetimeType, - Prettify & { isScheduleAt: true }> - > { - return new ColumnBuilder< - Type, - SpacetimeType, - Prettify & { isScheduleAt: true }> - >(this.typeBuilder, { - ...this.columnMetadata, - isScheduleAt: true, - }); - } } /** @@ -843,4 +822,4 @@ const t = { ); }, } as const; -export default t; \ No newline at end of file +export default t; diff --git a/crates/bindings-typescript/test/index.test.ts b/crates/bindings-typescript/test/index.test.ts index beeec2c057a..4a974ef94e1 100644 --- a/crates/bindings-typescript/test/index.test.ts +++ b/crates/bindings-typescript/test/index.test.ts @@ -19,4 +19,115 @@ describe('TypeBuilder', () => { }, }); }); + + it('builds the correct algebraic type for a sum type', () => { + const sumType = t.enum({ + a: t.string(), + b: t.number(), + }); + expect(sumType.algebraicType).toEqual({ + tag: 'Sum', + value: { + variants: [ + { name: 'a', algebraicType: AlgebraicType.String }, + { name: 'b', algebraicType: AlgebraicType.F64 }, + ], + }, + }); + }); + + it('builds a ColumnBuilder with an index, unique constraint, and primary key', () => { + const col = t.i32().index('btree').unique().primaryKey(); + expect(col.typeBuilder.algebraicType).toEqual({ + tag: 'I32', + }); + expect(col.columnMetadata.isPrimaryKey).toBe(true); + expect(col.columnMetadata.isUnique).toBe(true); + expect(col.columnMetadata.indexType).toBe('btree'); + expect(col.columnMetadata.isAutoIncrement).toBe(false); + expect(col.columnMetadata.isScheduleAt).toBe(false); + }); + + it('builds ColumnBuilders with the correct metadata', () => { + const indexCol = t.i32().index('btree'); + const uniqueCol = t.i32().unique(); + const primaryKeyCol = t.i32().primaryKey(); + const autoIncCol = t.i32().autoInc(); + + expect(indexCol.typeBuilder.algebraicType).toEqual({ + tag: 'I32', + }); + expect(indexCol.columnMetadata.isPrimaryKey).toBe(false); + expect(indexCol.columnMetadata.isUnique).toBe(false); + expect(indexCol.columnMetadata.indexType).toBe('btree'); + expect(indexCol.columnMetadata.isAutoIncrement).toBe(false); + expect(indexCol.columnMetadata.isScheduleAt).toBe(false); + + expect(uniqueCol.typeBuilder.algebraicType).toEqual({ + tag: 'I32', + }); + expect(uniqueCol.columnMetadata.isPrimaryKey).toBe(false); + expect(uniqueCol.columnMetadata.isUnique).toBe(true); + expect(uniqueCol.columnMetadata.indexType).toBeUndefined(); + expect(uniqueCol.columnMetadata.isAutoIncrement).toBe(false); + expect(uniqueCol.columnMetadata.isScheduleAt).toBe(false); + + expect(primaryKeyCol.typeBuilder.algebraicType).toEqual({ + tag: 'I32', + }); + expect(primaryKeyCol.columnMetadata.isPrimaryKey).toBe(true); + expect(primaryKeyCol.columnMetadata.isUnique).toBe(false); + expect(primaryKeyCol.columnMetadata.indexType).toBeUndefined(); + expect(primaryKeyCol.columnMetadata.isAutoIncrement).toBe(false); + expect(primaryKeyCol.columnMetadata.isScheduleAt).toBe(false); + + expect(autoIncCol.typeBuilder.algebraicType).toEqual({ + tag: 'I32', + }); + expect(autoIncCol.columnMetadata.isPrimaryKey).toBe(false); + expect(autoIncCol.columnMetadata.isUnique).toBe(false); + expect(autoIncCol.columnMetadata.indexType).toBeUndefined(); + expect(autoIncCol.columnMetadata.isAutoIncrement).toBe(true); + expect(autoIncCol.columnMetadata.isScheduleAt).toBe(false); + }); + + it('builds a ScheduleAt column with the correct type and metadata', () => { + const col = t.scheduleAt(); + expect(col.typeBuilder.algebraicType).toEqual({ + tag: 'Sum', + value: { + variants: [ + { + name: 'Interval', + algebraicType: { + tag: 'Product', + value: { + elements: [ + { + name: '__time_duration_micros__', + algebraicType: AlgebraicType.I64, + }, + ], + }, + }, + }, + { + name: 'Time', + algebraicType: { + tag: 'Product', + value: { + elements: [ + { + name: '__timestamp_micros_since_unix_epoch__', + algebraicType: AlgebraicType.I64, + }, + ], + }, + }, + }, + ], + }, + }); + expect(col.columnMetadata.isScheduleAt).toBe(true); + }); }); From 5c65074f18fbfd77fa9c82719e52a0913b8dbc61 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Fri, 5 Sep 2025 12:53:59 -0400 Subject: [PATCH 8/8] Removed function that shouldn't be there --- .../src/server/type_builders.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/bindings-typescript/src/server/type_builders.ts b/crates/bindings-typescript/src/server/type_builders.ts index ecdb618875c..9d5398cea89 100644 --- a/crates/bindings-typescript/src/server/type_builders.ts +++ b/crates/bindings-typescript/src/server/type_builders.ts @@ -242,24 +242,6 @@ export class TypeBuilder isAutoIncrement: true, }); } - - /** - * Specify this column as a schedule-at field - */ - scheduleAt(): ColumnBuilder< - Type, - SpacetimeType, - Prettify & { isScheduleAt: true }> - > { - return new ColumnBuilder< - Type, - SpacetimeType, - Prettify & { isScheduleAt: true }> - >(this, { - ...defaultMetadata, - isScheduleAt: true, - }); - } } export class U8Builder extends TypeBuilder {