From 166ccf6098d982f14abfc74d1002d351d2cb2e57 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Thu, 8 Jan 2026 11:08:45 -0800 Subject: [PATCH 01/11] Add a command to the typescript-test ci check that builds all of the different packages/examples --- .github/workflows/typescript-test.yml | 6 ++--- .../basic-react/src/module_bindings/index.ts | 5 ++-- .../examples/empty/package.json | 2 +- .../empty/src/module_bindings/index.ts | 5 ++-- .../src/module_bindings/index.ts | 2 +- crates/bindings-typescript/src/lib/schema.ts | 2 +- .../src/sdk/client_api/index.ts | 5 ++-- .../test-app/src/module_bindings/index.ts | 5 ++-- pnpm-lock.yaml | 24 ++----------------- 9 files changed, 20 insertions(+), 36 deletions(-) diff --git a/.github/workflows/typescript-test.yml b/.github/workflows/typescript-test.yml index c349fa6a707..de4f2bcc208 100644 --- a/.github/workflows/typescript-test.yml +++ b/.github/workflows/typescript-test.yml @@ -133,9 +133,9 @@ jobs: # run: | # spacetime logs quickstart-chat - - name: Check that quickstart-chat builds - working-directory: crates/bindings-typescript/examples/quickstart-chat - run: pnpm build + - name: Check that everything builds + working-directory: crates/bindings-typescript + run: pnpm -r --filter "./**" run build # - name: Run quickstart-chat tests # working-directory: examples/quickstart-chat diff --git a/crates/bindings-typescript/examples/basic-react/src/module_bindings/index.ts b/crates/bindings-typescript/examples/basic-react/src/module_bindings/index.ts index 37d22cfc611..568c49cb625 100644 --- a/crates/bindings-typescript/examples/basic-react/src/module_bindings/index.ts +++ b/crates/bindings-typescript/examples/basic-react/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.0 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.1 (commit 82d5a4f6c0d5f922792f63a5298d22ce3ca43a02). /* eslint-disable */ /* tslint:disable */ @@ -10,6 +10,7 @@ import { DbConnectionImpl as __DbConnectionImpl, SubscriptionBuilderImpl as __SubscriptionBuilderImpl, TypeBuilder as __TypeBuilder, + Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, procedureSchema as __procedureSchema, procedures as __procedures, @@ -84,7 +85,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '1.11.0' as const, + cliVersion: '1.11.1' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/bindings-typescript/examples/empty/package.json b/crates/bindings-typescript/examples/empty/package.json index daa3b650234..0cc0d7ae328 100644 --- a/crates/bindings-typescript/examples/empty/package.json +++ b/crates/bindings-typescript/examples/empty/package.json @@ -13,7 +13,7 @@ "spacetime:publish": "spacetime publish --project-path server --server maincloud" }, "dependencies": { - "spacetimedb": "^1.5.0" + "spacetimedb": "workspace:*" }, "devDependencies": { "typescript": "~5.6.2", diff --git a/crates/bindings-typescript/examples/empty/src/module_bindings/index.ts b/crates/bindings-typescript/examples/empty/src/module_bindings/index.ts index 37d22cfc611..568c49cb625 100644 --- a/crates/bindings-typescript/examples/empty/src/module_bindings/index.ts +++ b/crates/bindings-typescript/examples/empty/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.0 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.1 (commit 82d5a4f6c0d5f922792f63a5298d22ce3ca43a02). /* eslint-disable */ /* tslint:disable */ @@ -10,6 +10,7 @@ import { DbConnectionImpl as __DbConnectionImpl, SubscriptionBuilderImpl as __SubscriptionBuilderImpl, TypeBuilder as __TypeBuilder, + Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, procedureSchema as __procedureSchema, procedures as __procedures, @@ -84,7 +85,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '1.11.0' as const, + cliVersion: '1.11.1' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/bindings-typescript/examples/quickstart-chat/src/module_bindings/index.ts b/crates/bindings-typescript/examples/quickstart-chat/src/module_bindings/index.ts index 42d2835245a..cab550e9a04 100644 --- a/crates/bindings-typescript/examples/quickstart-chat/src/module_bindings/index.ts +++ b/crates/bindings-typescript/examples/quickstart-chat/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.1 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.1 (commit 82d5a4f6c0d5f922792f63a5298d22ce3ca43a02). /* eslint-disable */ /* tslint:disable */ diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index 3dcb886f155..ae983ee13df 100644 --- a/crates/bindings-typescript/src/lib/schema.ts +++ b/crates/bindings-typescript/src/lib/schema.ts @@ -77,7 +77,7 @@ type TablesToSchema = { }; }; -interface TableToSchema extends UntypedTableDef { +export interface TableToSchema extends UntypedTableDef { name: T['tableName']; accessorName: CamelCase; columns: T['rowType']['row']; diff --git a/crates/bindings-typescript/src/sdk/client_api/index.ts b/crates/bindings-typescript/src/sdk/client_api/index.ts index 1dffeccfdd1..2a8b079b738 100644 --- a/crates/bindings-typescript/src/sdk/client_api/index.ts +++ b/crates/bindings-typescript/src/sdk/client_api/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.0 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.1 (commit 82d5a4f6c0d5f922792f63a5298d22ce3ca43a02). /* eslint-disable */ /* tslint:disable */ @@ -10,6 +10,7 @@ import { DbConnectionImpl as __DbConnectionImpl, SubscriptionBuilderImpl as __SubscriptionBuilderImpl, TypeBuilder as __TypeBuilder, + Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, procedureSchema as __procedureSchema, procedures as __procedures, @@ -118,7 +119,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '1.11.0' as const, + cliVersion: '1.11.1' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/bindings-typescript/test-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-app/src/module_bindings/index.ts index 46ef1b99735..8a738b07980 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.0 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.1 (commit 82d5a4f6c0d5f922792f63a5298d22ce3ca43a02). /* eslint-disable */ /* tslint:disable */ @@ -10,6 +10,7 @@ import { DbConnectionImpl as __DbConnectionImpl, SubscriptionBuilderImpl as __SubscriptionBuilderImpl, TypeBuilder as __TypeBuilder, + Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, procedureSchema as __procedureSchema, procedures as __procedures, @@ -109,7 +110,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '1.11.0' as const, + cliVersion: '1.11.1' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 773d5cebd69..74a5e8f13bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,8 +158,8 @@ importers: crates/bindings-typescript/examples/empty: dependencies: spacetimedb: - specifier: ^1.5.0 - version: 1.6.2(react@19.2.0)(undici@6.21.3) + specifier: workspace:* + version: link:../.. devDependencies: typescript: specifier: ~5.6.2 @@ -7963,17 +7963,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - spacetimedb@1.6.2: - resolution: {integrity: sha512-XOdIgnTT1j2wZNiPdEIiv9uzhpnbqO0Rk/kOxVA9XhMMmrbtS3bXduBf1MzcaS/gJzXFK+Tf27j1HVa5SNbhLQ==} - peerDependencies: - react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 - undici: ^6.19.2 - peerDependenciesMeta: - react: - optional: true - undici: - optional: true - spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} @@ -19164,15 +19153,6 @@ snapshots: space-separated-tokens@2.0.2: {} - spacetimedb@1.6.2(react@19.2.0)(undici@6.21.3): - dependencies: - base64-js: 1.5.1 - fast-text-encoding: 1.0.6 - prettier: 3.6.2 - optionalDependencies: - react: 19.2.0 - undici: 6.21.3 - spdy-transport@3.0.0: dependencies: debug: 4.4.3 From 902af09c55b418c987000e739eb176a3368296ca Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 9 Jan 2026 08:29:25 -0800 Subject: [PATCH 02/11] fmt --- crates/bindings-typescript/src/lib/schema.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bindings-typescript/src/lib/schema.ts b/crates/bindings-typescript/src/lib/schema.ts index ae983ee13df..57cf9a62ded 100644 --- a/crates/bindings-typescript/src/lib/schema.ts +++ b/crates/bindings-typescript/src/lib/schema.ts @@ -77,7 +77,8 @@ type TablesToSchema = { }; }; -export interface TableToSchema extends UntypedTableDef { +export interface TableToSchema + extends UntypedTableDef { name: T['tableName']; accessorName: CamelCase; columns: T['rowType']['row']; From 2c24facf850c87fec8070b766ea39cf355a0af05 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 9 Jan 2026 10:11:27 -0800 Subject: [PATCH 03/11] Use the generate command that formats --- .github/workflows/typescript-test.yml | 6 ++---- package.json | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/typescript-test.yml b/.github/workflows/typescript-test.yml index bf5b64f507f..1d37d26cb6c 100644 --- a/.github/workflows/typescript-test.yml +++ b/.github/workflows/typescript-test.yml @@ -103,11 +103,9 @@ jobs: spacetime server clear -y - name: Generate client bindings - working-directory: templates/quickstart-chat-typescript/spacetimedb + working-directory: templates/quickstart-chat-typescript run: | - spacetime generate --lang typescript --out-dir ../src/module_bindings - cd ../../../crates/bindings-typescript - pnpm format + pnpm generate - name: Check for changes working-directory: templates/quickstart-chat-typescript diff --git a/package.json b/package.json index 64fa9419780..e7e348b58bd 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "engines": { "node": ">=18.0.0", "pnpm": ">=9.0.0" }, "type": "module", "scripts": { - "format": "pnpm --filter ./crates/bindings-typescript run format && pnpm --filter ./docs run format && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run format && pnpm --filter ./crates/bindings-typescript/test-app run format", - "lint": "pnpm --filter ./crates/bindings-typescript run lint && pnpm --filter ./docs run lint && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run lint && pnpm --filter ./crates/bindings-typescript/test-app run lint", + "format": "pnpm --filter ./crates/bindings-typescript run format && pnpm --filter ./docs run format && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run format && pnpm --filter ./crates/bindings-typescript/test-app run format && pnpm --filter ./templates/quickstart-chat-typescript run format && pnpm --filter ./templates/basic-react run format", + "lint": "pnpm --filter ./crates/bindings-typescript run lint && pnpm --filter ./docs run lint && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run lint && pnpm --filter ./crates/bindings-typescript/test-app run lint && pnpm --filter ./templates/quickstart-chat-typescript run lint && pnpm --filter ./templates/basic-react run lint", "build": "pnpm --filter ./crates/bindings-typescript run build && pnpm --filter ./docs run build && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run build && pnpm --filter ./crates/bindings-typescript/test-app run build", "test": "pnpm --filter ./crates/bindings-typescript run test && pnpm --filter ./docs run test && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run test && pnpm --filter ./crates/bindings-typescript/test-app run test", - "generate": "pnpm --filter ./crates/bindings-typescript run generate && pnpm --filter ./docs run generate && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run generate && pnpm --filter ./crates/bindings-typescript/test-app run generate", + "generate": "pnpm --filter ./crates/bindings-typescript run generate && pnpm --filter ./docs run generate && pnpm --filter ./crates/bindings-typescript/test-app run generate && pnpm --filter ./templates/quickstart-chat-typescript run generate && pnpm --filter ./templates/basic-react run generate", "clean": "pnpm -r exec rimraf dist .tsbuildinfo coverage" }, "devDependencies": { From 20aa6cc8ff12d2de4d1b8b790542918aa9e90474 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 9 Jan 2026 10:24:02 -0800 Subject: [PATCH 04/11] Simplify some commands --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e7e348b58bd..fb9b8211531 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "engines": { "node": ">=18.0.0", "pnpm": ">=9.0.0" }, "type": "module", "scripts": { - "format": "pnpm --filter ./crates/bindings-typescript run format && pnpm --filter ./docs run format && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run format && pnpm --filter ./crates/bindings-typescript/test-app run format && pnpm --filter ./templates/quickstart-chat-typescript run format && pnpm --filter ./templates/basic-react run format", - "lint": "pnpm --filter ./crates/bindings-typescript run lint && pnpm --filter ./docs run lint && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run lint && pnpm --filter ./crates/bindings-typescript/test-app run lint && pnpm --filter ./templates/quickstart-chat-typescript run lint && pnpm --filter ./templates/basic-react run lint", - "build": "pnpm --filter ./crates/bindings-typescript run build && pnpm --filter ./docs run build && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run build && pnpm --filter ./crates/bindings-typescript/test-app run build", + "format": "pnpm --filter ./crates/bindings-typescript run format && pnpm --filter ./docs run format && pnpm --filter ./crates/bindings-typescript/test-app run format && pnpm -r --filter './templates/**' run format", + "lint": "pnpm --filter ./crates/bindings-typescript run lint && pnpm --filter ./docs run lint && pnpm --filter ./crates/bindings-typescript/test-app run lint && pnpm -r --filter './templates/**' run lint", + "build": "pnpm --filter ./crates/bindings-typescript run build && pnpm --filter ./docs run build && pnpm --filter ./crates/bindings-typescript/test-app run build && pnpm -r --filter './templates/**' run build", "test": "pnpm --filter ./crates/bindings-typescript run test && pnpm --filter ./docs run test && pnpm --filter ./crates/bindings-typescript/examples/quickstart-chat run test && pnpm --filter ./crates/bindings-typescript/test-app run test", - "generate": "pnpm --filter ./crates/bindings-typescript run generate && pnpm --filter ./docs run generate && pnpm --filter ./crates/bindings-typescript/test-app run generate && pnpm --filter ./templates/quickstart-chat-typescript run generate && pnpm --filter ./templates/basic-react run generate", + "generate": "pnpm --filter ./crates/bindings-typescript run generate && pnpm --filter ./docs run generate && pnpm --filter ./crates/bindings-typescript/test-app run generate && pnpm -r --filter './templates/**' run generate", "clean": "pnpm -r exec rimraf dist .tsbuildinfo coverage" }, "devDependencies": { From 43f8e39aa089984cd5bfc6c65416acd31a8ba3b4 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Fri, 9 Jan 2026 10:26:19 -0800 Subject: [PATCH 05/11] Build all ts templates in ci --- .github/workflows/typescript-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/typescript-test.yml b/.github/workflows/typescript-test.yml index 1d37d26cb6c..c04580021da 100644 --- a/.github/workflows/typescript-test.yml +++ b/.github/workflows/typescript-test.yml @@ -135,6 +135,10 @@ jobs: working-directory: templates/quickstart-chat-typescript run: pnpm build + - name: Check that templates build + working-directory: templates/ + run: pnpm -r --filter "./**" run build + - name: Check that subdirectories build working-directory: crates/bindings-typescript run: pnpm -r --filter "./**" run build From e88f05bc0173912ed8c6fb67afb7e54483a194c3 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Mon, 12 Jan 2026 09:17:34 -0800 Subject: [PATCH 06/11] WIP --- crates/bindings-typescript/src/index.ts | 1 + crates/bindings-typescript/src/lib/query.ts | 778 +++++++++++++++++ crates/bindings-typescript/src/lib/views.ts | 2 +- .../src/sdk/client_api/index.ts | 9 +- .../bindings-typescript/src/server/index.ts | 2 +- .../bindings-typescript/src/server/query.ts | 779 +----------------- .../test-app/src/module_bindings/index.ts | 9 +- crates/codegen/src/typescript.rs | 7 + .../codegen__codegen_typescript.snap | 4 + .../src/module_bindings/add_reducer.ts | 2 +- .../basic-react/src/module_bindings/index.ts | 77 +- .../src/module_bindings/on_connect_reducer.ts | 2 +- .../module_bindings/on_disconnect_reducer.ts | 2 +- .../src/module_bindings/person_table.ts | 2 +- .../src/module_bindings/person_type.ts | 6 +- .../src/module_bindings/say_hello_reducer.ts | 2 +- .../src/module_bindings/add_reducer.ts | 2 +- .../src/module_bindings/index.ts | 77 +- .../src/module_bindings/on_connect_reducer.ts | 2 +- .../module_bindings/on_disconnect_reducer.ts | 2 +- .../src/module_bindings/person_table.ts | 2 +- .../src/module_bindings/person_type.ts | 6 +- .../src/module_bindings/say_hello_reducer.ts | 2 +- .../src/module_bindings/index.ts | 108 ++- .../src/module_bindings/init_type.ts | 6 +- .../src/module_bindings/message_table.ts | 2 +- .../src/module_bindings/message_type.ts | 6 +- .../src/module_bindings/on_connect_reducer.ts | 2 +- .../src/module_bindings/on_connect_type.ts | 6 +- .../module_bindings/on_disconnect_reducer.ts | 2 +- .../src/module_bindings/on_disconnect_type.ts | 6 +- .../module_bindings/send_message_reducer.ts | 2 +- .../src/module_bindings/send_message_type.ts | 6 +- .../src/module_bindings/set_name_reducer.ts | 2 +- .../src/module_bindings/set_name_type.ts | 6 +- .../src/module_bindings/user_table.ts | 2 +- .../src/module_bindings/user_type.ts | 6 +- 37 files changed, 1007 insertions(+), 932 deletions(-) create mode 100644 crates/bindings-typescript/src/lib/query.ts diff --git a/crates/bindings-typescript/src/index.ts b/crates/bindings-typescript/src/index.ts index b21040d450a..7061d1448ce 100644 --- a/crates/bindings-typescript/src/index.ts +++ b/crates/bindings-typescript/src/index.ts @@ -11,4 +11,5 @@ export * from './lib/util'; export * from './lib/identity'; export * from './lib/option'; export * from './lib/result'; +export * from './lib/query'; export * from './sdk'; diff --git a/crates/bindings-typescript/src/lib/query.ts b/crates/bindings-typescript/src/lib/query.ts new file mode 100644 index 00000000000..fa9c543b057 --- /dev/null +++ b/crates/bindings-typescript/src/lib/query.ts @@ -0,0 +1,778 @@ +import { ConnectionId } from './connection_id'; +import { Identity } from './identity'; +import type { ColumnIndex, IndexColumns, IndexOpts } from './indexes'; +import type { UntypedSchemaDef } from './schema'; +import type { TableSchema } from './table_schema'; +import type { + ColumnBuilder, + ColumnMetadata, + RowBuilder, + TypeBuilder, +} from './type_builders'; + +/** + * Helper to get the set of table names. + */ +export type TableNames = + SchemaDef['tables'][number]['name'] & string; + +/** helper: pick the table def object from the schema by its name */ +export type TableDefByName< + SchemaDef extends UntypedSchemaDef, + Name extends TableNames, +> = Extract; + +// internal only — NOT exported. +// This is how we make sure queries are only created with our helpers. +const QueryBrand = Symbol('QueryBrand'); + +export interface TableTypedQuery { + readonly [QueryBrand]: true; + readonly __table?: TableDef; +} + +export interface RowTypedQuery { + readonly [QueryBrand]: true; + // Phantom type to track the row type. + readonly __row?: Row; + readonly __algebraicType?: ST; +} + +export type Query = RowTypedQuery< + RowType, + TableDef['rowType'] +>; + +export const isRowTypedQuery = (val: unknown): val is RowTypedQuery => + !!val && typeof val === 'object' && QueryBrand in (val as object); + +export const isTypedQuery = (val: unknown): val is TableTypedQuery => + !!val && typeof val === 'object' && QueryBrand in (val as object); + +export function toSql(q: Query): string { + return (q as unknown as { toSql(): string }).toSql(); +} + +// A query builder with a single table. +type From = Readonly<{ + where( + predicate: (row: RowExpr) => BooleanExpr + ): From; + rightSemijoin( + other: TableRef, + on: ( + left: IndexedRowExpr, + right: IndexedRowExpr + ) => EqExpr + ): SemijoinBuilder; + leftSemijoin( + other: TableRef, + on: ( + left: IndexedRowExpr, + right: IndexedRowExpr + ) => EqExpr + ): SemijoinBuilder; + build(): Query; +}>; + +// A query builder with a semijoin. +type SemijoinBuilder = Readonly<{ + where( + predicate: (row: RowExpr) => BooleanExpr + ): SemijoinBuilder; + build(): Query; +}>; + +class SemijoinImpl + implements SemijoinBuilder, TableTypedQuery +{ + readonly [QueryBrand] = true; + readonly type = 'semijoin' as const; + constructor( + readonly sourceQuery: FromBuilder, + readonly filterQuery: FromBuilder, + readonly joinCondition: EqExpr + ) { + if (sourceQuery.table.name === filterQuery.table.name) { + // TODO: Handle aliasing properly instead of just forbidding it. + throw new Error('Cannot semijoin a table to itself'); + } + } + + build(): Query { + return this as Query; + } + + where( + predicate: (row: RowExpr) => BooleanExpr + ): SemijoinImpl { + const nextSourceQuery = this.sourceQuery.where(predicate); + return new SemijoinImpl( + nextSourceQuery, + this.filterQuery, + this.joinCondition + ); + } + + toSql(): string { + const left = this.filterQuery; + const right = this.sourceQuery; + const leftTable = quoteIdentifier(left.table.name); + const rightTable = quoteIdentifier(right.table.name); + let sql = `SELECT ${rightTable}.* FROM ${leftTable} JOIN ${rightTable} ON ${booleanExprToSql(this.joinCondition)}`; + + const clauses: string[] = []; + if (left.whereClause) { + clauses.push(booleanExprToSql(left.whereClause)); + } + if (right.whereClause) { + clauses.push(booleanExprToSql(right.whereClause)); + } + + if (clauses.length > 0) { + const whereSql = + clauses.length === 1 + ? clauses[0] + : clauses.map(wrapInParens).join(' AND '); + sql += ` WHERE ${whereSql}`; + } + + return sql; + } +} + +class FromBuilder + implements From, TableTypedQuery +{ + readonly [QueryBrand] = true; + constructor( + readonly table: TableRef, + readonly whereClause?: BooleanExpr + ) {} + + where( + predicate: (row: RowExpr) => BooleanExpr + ): FromBuilder { + const newCondition = predicate(this.table.cols); + const nextWhere = this.whereClause + ? and(this.whereClause, newCondition) + : newCondition; + return new FromBuilder(this.table, nextWhere); + } + + rightSemijoin( + right: TableRef, + on: ( + left: IndexedRowExpr, + right: IndexedRowExpr + ) => EqExpr + ): SemijoinBuilder { + const sourceQuery = new FromBuilder(right); + const joinCondition = on( + this.table.indexedCols, + right.indexedCols + ) as EqExpr; + return new SemijoinImpl(sourceQuery, this, joinCondition); + } + + leftSemijoin( + right: TableRef, + on: ( + left: IndexedRowExpr, + right: IndexedRowExpr + ) => EqExpr + ): SemijoinBuilder { + const filterQuery = new FromBuilder(right); + const joinCondition = on( + this.table.indexedCols, + right.indexedCols + ) as EqExpr; + return new SemijoinImpl(this, filterQuery, joinCondition); + } + + toSql(): string { + return renderSelectSqlWithJoins(this.table, this.whereClause); + } + + build(): Query { + return this as Query; + } +} + +export type QueryBuilder = { + readonly [Tbl in SchemaDef['tables'][number] as Tbl['name']]: TableRef & + From; +} & {}; + +/** + * A runtime reference to a table. This materializes the RowExpr for us. + * TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work. + */ +export type TableRef = Readonly<{ + type: 'table'; + name: TableDef['name']; + cols: RowExpr; + indexedCols: IndexedRowExpr; + // Maybe redundant. + tableDef: TableDef; +}>; + +class TableRefImpl + implements TableRef, From +{ + readonly type = 'table' as const; + name: string; + cols: RowExpr; + indexedCols: IndexedRowExpr; + tableDef: TableDef; + constructor(tableDef: TableDef) { + this.name = tableDef.name; + this.cols = createRowExpr(tableDef); + // this.indexedCols = createIndexedRowExpr(tableDef, this.cols); + // TODO: we could create an indexedRowExpr to avoid having the extra columns. + // Right now, the objects we pass will actually have all the columns, but the + // type system will consider it an error. + this.indexedCols = this.cols; + this.tableDef = tableDef; + Object.freeze(this); + } + + asFrom(): FromBuilder { + return new FromBuilder(this); + } + + rightSemijoin( + other: TableRef, + on: ( + left: IndexedRowExpr, + right: IndexedRowExpr + ) => EqExpr + ): SemijoinBuilder { + return this.asFrom().rightSemijoin(other, on); + } + + leftSemijoin( + other: TableRef, + on: ( + left: IndexedRowExpr, + right: IndexedRowExpr + ) => EqExpr + ): SemijoinBuilder { + return this.asFrom().leftSemijoin(other, on); + } + + build(): Query { + return this.asFrom().build(); + } + + toSql(): string { + return this.asFrom().toSql(); + } + + where( + predicate: (row: RowExpr) => BooleanExpr + ): FromBuilder { + return this.asFrom().where(predicate); + } +} + +export type RefSource = + | TableRef + | { ref(): TableRef }; + +export function createTableRefFromDef( + tableDef: TableDef +): TableRef { + return new TableRefImpl(tableDef); +} + +export function makeQueryBuilder( + schema: SchemaDef +): QueryBuilder { + const qb = Object.create(null) as QueryBuilder; + for (const table of schema.tables) { + const ref = createTableRefFromDef( + table as TableDefByName> + ); + (qb as Record>)[table.name] = ref; + } + return Object.freeze(qb) as QueryBuilder; +} + +function createRowExpr( + tableDef: TableDef +): RowExpr { + const row: Record> = {}; + for (const columnName of Object.keys(tableDef.columns) as Array< + keyof TableDef['columns'] & string + >) { + const columnBuilder = tableDef.columns[columnName]; + const column = new ColumnExpression( + tableDef.name, + columnName, + columnBuilder.typeBuilder.algebraicType as InferSpacetimeTypeOfColumn< + TableDef, + typeof columnName + > + ); + row[columnName] = Object.freeze(column); + } + return Object.freeze(row) as RowExpr; +} + +export function from( + source: RefSource +): From { + return new FromBuilder(resolveTableRef(source)); +} + +function resolveTableRef( + source: RefSource +): TableRef { + if (typeof (source as { ref?: unknown }).ref === 'function') { + return (source as { ref(): TableRef }).ref(); + } + return source as TableRef; +} + +function renderSelectSqlWithJoins( + table: TableRef
, + where?: BooleanExpr
, + extraClauses: readonly string[] = [] +): string { + const quotedTable = quoteIdentifier(table.name); + const sql = `SELECT * FROM ${quotedTable}`; + const clauses: string[] = []; + if (where) clauses.push(booleanExprToSql(where)); + clauses.push(...extraClauses); + if (clauses.length === 0) return sql; + const whereSql = + clauses.length === 1 ? clauses[0] : clauses.map(wrapInParens).join(' AND '); + return `${sql} WHERE ${whereSql}`; +} + +// TODO: Just use UntypedTableDef if they end up being the same. +export type TypedTableDef< + Columns extends Record< + string, + ColumnBuilder> + > = Record>>, +> = { + name: string; + columns: Columns; + indexes: readonly IndexOpts[]; + rowType: RowBuilder['algebraicType']['value']; +}; + +export type TableSchemaAsTableDef< + TSchema extends TableSchema, +> = { + name: TSchema['tableName']; + columns: TSchema['rowType']['row']; + indexes: TSchema['idxs']; +}; + +type RowType = { + [K in keyof TableDef['columns']]: TableDef['columns'][K] extends ColumnBuilder< + infer T, + any, + any + > + ? T + : never; +}; + +// TODO: Consider making a smaller version of these types that doesn't expose the internals. +// Restricting it later should not break anyone in practice. +export type ColumnExpr< + TableDef extends TypedTableDef, + ColumnName extends ColumnNames, +> = ColumnExpression; + +type ColumnSpacetimeType> = + Col extends ColumnExpr + ? InferSpacetimeTypeOfColumn + : never; + +// TODO: This checks that they match, but we also need to make sure that they are comparable types, +// since you can use product types at all. +type ColumnSameSpacetime< + ThisTable extends TypedTableDef, + ThisCol extends ColumnNames, + OtherCol extends ColumnExpr, +> = [InferSpacetimeTypeOfColumn] extends [ + ColumnSpacetimeType, +] + ? [ColumnSpacetimeType] extends [ + InferSpacetimeTypeOfColumn, + ] + ? OtherCol + : never + : never; + +// Helper to get the table back from a column. +type ExtractTable> = + Col extends ColumnExpr ? T : never; + +export class ColumnExpression< + TableDef extends TypedTableDef, + ColumnName extends ColumnNames, +> { + readonly type = 'column' as const; + readonly column: ColumnName; + readonly table: TableDef['name']; + // phantom: actual runtime value is undefined + readonly tsValueType?: RowType[ColumnName]; + readonly spacetimeType: InferSpacetimeTypeOfColumn; + + constructor( + table: TableDef['name'], + column: ColumnName, + spacetimeType: InferSpacetimeTypeOfColumn + ) { + this.table = table; + this.column = column; + this.spacetimeType = spacetimeType; + } + + eq(literal: LiteralValue & RowType[ColumnName]): EqExpr; + eq>( + value: ColumnSameSpacetime + ): EqExpr>; + + // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. + eq(x: any): any { + return { + type: 'eq', + left: this as unknown as ValueExpr, + right: normalizeValue(x) as ValueExpr, + } as EqExpr; + } + + lt( + literal: LiteralValue & RowType[ColumnName] + ): BooleanExpr; + lt>( + value: ColumnSameSpacetime + ): BooleanExpr>; + + // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. + lt(x: any): any { + return { + type: 'lt', + left: this as unknown as ValueExpr, + right: normalizeValue(x) as ValueExpr, + } as BooleanExpr; + } + lte( + literal: LiteralValue & RowType[ColumnName] + ): BooleanExpr; + lte>( + value: ColumnSameSpacetime + ): BooleanExpr>; + + // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. + lte(x: any): any { + return { + type: 'lte', + left: this as unknown as ValueExpr, + right: normalizeValue(x) as ValueExpr, + } as BooleanExpr; + } + + gt( + literal: LiteralValue & RowType[ColumnName] + ): BooleanExpr; + gt>( + value: ColumnSameSpacetime + ): BooleanExpr>; + + // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. + gt(x: any): any { + return { + type: 'gt', + left: this as unknown as ValueExpr, + right: normalizeValue(x) as ValueExpr, + } as BooleanExpr; + } + gte( + literal: LiteralValue & RowType[ColumnName] + ): BooleanExpr; + gte>( + value: ColumnSameSpacetime + ): BooleanExpr>; + + // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. + gte(x: any): any { + return { + type: 'gte', + left: this as unknown as ValueExpr, + right: normalizeValue(x) as ValueExpr, + } as BooleanExpr; + } +} + +/** + * Helper to get the spacetime type of a column. + */ +type InferSpacetimeTypeOfColumn< + TableDef extends TypedTableDef, + ColumnName extends ColumnNames, +> = + TableDef['columns'][ColumnName]['typeBuilder'] extends TypeBuilder< + any, + infer U + > + ? U + : never; + +type ColumnNames = keyof RowType & + string; + +// For composite indexes, we only consider it as an index over the first column in the index. +type FirstIndexColumn> = + IndexColumns extends readonly [infer Head extends string, ...infer _Rest] + ? Head + : never; + +// Columns that are indexed by something in the indexes: [...] part. +type ExplicitIndexedColumns = + TableDef['indexes'][number] extends infer I + ? I extends IndexOpts> + ? FirstIndexColumn & ColumnNames + : never + : never; + +// Columns with an index defined on the column definition. +type MetadataIndexedColumns = { + [K in ColumnNames]: ColumnIndex< + K, + TableDef['columns'][K]['columnMetadata'] + > extends never + ? never + : K; +}[ColumnNames]; + +export type IndexedColumnNames = + | ExplicitIndexedColumns + | MetadataIndexedColumns; + +export type IndexedRowExpr = Readonly<{ + readonly [C in IndexedColumnNames]: ColumnExpr; +}>; + +/** + * Acts as a row when writing filters for queries. It is a way to get column references. + */ +export type RowExpr = Readonly<{ + readonly [C in ColumnNames]: ColumnExpr; +}>; + +/** + * Union of ColumnExprs from Table whose spacetimeType is compatible with Value + * (produces a union of ColumnExpr for matching columns). + */ +export type ColumnExprForValue
= { + [C in ColumnNames
]: InferSpacetimeTypeOfColumn extends Value + ? ColumnExpr + : never; +}[ColumnNames
]; + +type LiteralValue = + | string + | number + | bigint + | boolean + | Identity + | ConnectionId; + +type ValueLike = LiteralValue | ColumnExpr | LiteralExpr; +type ValueInput = + | ValueLike + | ValueExpr; + +export type ValueExpr = + | LiteralExpr + | ColumnExprForValue; + +type LiteralExpr = { + type: 'literal'; + value: Value; +}; + +export function literal( + value: Value +): ValueExpr { + return { type: 'literal', value }; +} + +// This is here to take literal values and wrap them in an AST node. +function normalizeValue(val: ValueInput): ValueExpr { + if ((val as LiteralExpr).type === 'literal') + return val as LiteralExpr; + if ( + typeof val === 'object' && + val != null && + 'type' in (val as any) && + (val as any).type === 'column' + ) { + return val as ColumnExpr; + } + return literal(val as LiteralValue); +} + +type EqExpr
= { + type: 'eq'; + left: ValueExpr; + right: ValueExpr; +} & { + _tableType?: Table; +}; + +type BooleanExpr
= ( + | { + type: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte'; + left: ValueExpr; + right: ValueExpr; + } + | { + type: 'and'; + clauses: readonly [ + BooleanExpr
, + BooleanExpr
, + ...BooleanExpr
[], + ]; + } + | { + type: 'or'; + clauses: readonly [ + BooleanExpr
, + BooleanExpr
, + ...BooleanExpr
[], + ]; + } + | { + type: 'not'; + clause: BooleanExpr
; + } +) & { + _tableType?: Table; + // readonly [BooleanExprBrand]: Table?; +}; + +export function not( + clause: BooleanExpr +): BooleanExpr { + return { type: 'not', clause }; +} + +export function and( + ...clauses: readonly [BooleanExpr, BooleanExpr, ...BooleanExpr[]] +): BooleanExpr { + return { type: 'and', clauses }; +} + +export function or( + ...clauses: readonly [BooleanExpr, BooleanExpr, ...BooleanExpr[]] +): BooleanExpr { + return { type: 'or', clauses }; +} + +function booleanExprToSql
( + expr: BooleanExpr
, + tableAlias?: string +): string { + switch (expr.type) { + case 'eq': + return `${valueExprToSql(expr.left, tableAlias)} = ${valueExprToSql(expr.right, tableAlias)}`; + case 'ne': + return `${valueExprToSql(expr.left, tableAlias)} <> ${valueExprToSql(expr.right, tableAlias)}`; + case 'gt': + return `${valueExprToSql(expr.left, tableAlias)} > ${valueExprToSql(expr.right, tableAlias)}`; + case 'gte': + return `${valueExprToSql(expr.left, tableAlias)} >= ${valueExprToSql(expr.right, tableAlias)}`; + case 'lt': + return `${valueExprToSql(expr.left, tableAlias)} < ${valueExprToSql(expr.right, tableAlias)}`; + case 'lte': + return `${valueExprToSql(expr.left, tableAlias)} <= ${valueExprToSql(expr.right, tableAlias)}`; + case 'and': + return expr.clauses + .map(c => booleanExprToSql(c, tableAlias)) + .map(wrapInParens) + .join(' AND '); + case 'or': + return expr.clauses + .map(c => booleanExprToSql(c, tableAlias)) + .map(wrapInParens) + .join(' OR '); + case 'not': + return `NOT ${wrapInParens(booleanExprToSql(expr.clause, tableAlias))}`; + } +} + +function wrapInParens(sql: string): string { + return `(${sql})`; +} + +function valueExprToSql
( + expr: ValueExpr, + tableAlias?: string +): string { + if (isLiteralExpr(expr)) { + return literalValueToSql(expr.value); + } + const table = tableAlias ?? expr.table; + return `${quoteIdentifier(table)}.${quoteIdentifier(expr.column)}`; +} + +function literalValueToSql(value: unknown): string { + if (value === null || value === undefined) { + return 'NULL'; + } + if (value instanceof Identity || value instanceof ConnectionId) { + // We use this hex string syntax. + return `0x${value.toHexString()}`; + } + switch (typeof value) { + case 'number': + case 'bigint': + return String(value); + case 'boolean': + return value ? 'TRUE' : 'FALSE'; + case 'string': + return `'${value.replace(/'/g, "''")}'`; + default: + // It might be safer to error here? + return `'${JSON.stringify(value).replace(/'/g, "''")}'`; + } +} + +function quoteIdentifier(name: string): string { + return `"${name.replace(/"/g, '""')}"`; +} + +function isLiteralExpr( + expr: ValueExpr +): expr is LiteralExpr { + return (expr as LiteralExpr).type === 'literal'; +} + +// TODO: Fix this. +function _createIndexedRowExpr( + tableDef: TableDef, + cols: RowExpr +): IndexedRowExpr { + const indexed = new Set>(); + for (const idx of tableDef.indexes) { + if ('columns' in idx) { + const [first] = idx.columns; + if (first) indexed.add(first); + } else if ('column' in idx) { + indexed.add(idx.column); + } + } + const pickedEntries = [...indexed].map(name => [name, cols[name]]); + return Object.freeze( + Object.fromEntries(pickedEntries) + ) as IndexedRowExpr; +} diff --git a/crates/bindings-typescript/src/lib/views.ts b/crates/bindings-typescript/src/lib/views.ts index 82f9f069393..056f25af665 100644 --- a/crates/bindings-typescript/src/lib/views.ts +++ b/crates/bindings-typescript/src/lib/views.ts @@ -21,7 +21,7 @@ import { type TypeBuilder, } from './type_builders'; import { bsatnBaseSize, toPascalCase } from './util'; -import { type QueryBuilder, type RowTypedQuery } from '../server/query'; +import { type QueryBuilder, type RowTypedQuery } from './query'; export type ViewCtx = Readonly<{ sender: Identity; diff --git a/crates/bindings-typescript/src/sdk/client_api/index.ts b/crates/bindings-typescript/src/sdk/client_api/index.ts index 1dffeccfdd1..92ccd5a468e 100644 --- a/crates/bindings-typescript/src/sdk/client_api/index.ts +++ b/crates/bindings-typescript/src/sdk/client_api/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.0 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.2 (commit 902af09c55b418c987000e739eb176a3368296ca). /* eslint-disable */ /* tslint:disable */ @@ -10,7 +10,9 @@ import { DbConnectionImpl as __DbConnectionImpl, SubscriptionBuilderImpl as __SubscriptionBuilderImpl, TypeBuilder as __TypeBuilder, + Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -118,7 +120,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '1.11.0' as const, + cliVersion: '1.11.2' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, @@ -132,6 +134,9 @@ const REMOTE_MODULE = { /** The tables available in this remote SpacetimeDB module. */ export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); +/** A typed query builder for this remote SpacetimeDB module. */ +export const query = __makeQueryBuilder(tablesSchema.schemaType); + /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( reducersSchema.reducersType.reducers diff --git a/crates/bindings-typescript/src/server/index.ts b/crates/bindings-typescript/src/server/index.ts index 2147b4ab7d1..61df4bb591c 100644 --- a/crates/bindings-typescript/src/server/index.ts +++ b/crates/bindings-typescript/src/server/index.ts @@ -5,7 +5,7 @@ export { reducers } from '../lib/reducers'; export { SenderError, SpacetimeHostError, errors } from './errors'; export { type Reducer, type ReducerCtx } from '../lib/reducers'; export { type DbView } from './db_view'; -export { and, or, not } from './query'; +export * from './query'; export type { ProcedureCtx, TransactionCtx } from '../lib/procedures'; export { toCamelCase } from '../lib/util'; export { type Uuid } from '../lib/uuid'; diff --git a/crates/bindings-typescript/src/server/query.ts b/crates/bindings-typescript/src/server/query.ts index 0427e6753e1..afeafcdfe03 100644 --- a/crates/bindings-typescript/src/server/query.ts +++ b/crates/bindings-typescript/src/server/query.ts @@ -1,778 +1 @@ -import { ConnectionId } from '../lib/connection_id'; -import { Identity } from '../lib/identity'; -import type { ColumnIndex, IndexColumns, IndexOpts } from '../lib/indexes'; -import type { UntypedSchemaDef } from '../lib/schema'; -import type { TableSchema } from '../lib/table_schema'; -import type { - ColumnBuilder, - ColumnMetadata, - RowBuilder, - TypeBuilder, -} from '../lib/type_builders'; - -/** - * Helper to get the set of table names. - */ -export type TableNames = - SchemaDef['tables'][number]['name'] & string; - -/** helper: pick the table def object from the schema by its name */ -export type TableDefByName< - SchemaDef extends UntypedSchemaDef, - Name extends TableNames, -> = Extract; - -// internal only — NOT exported. -// This is how we make sure queries are only created with our helpers. -const QueryBrand = Symbol('QueryBrand'); - -export interface TableTypedQuery { - readonly [QueryBrand]: true; - readonly __table?: TableDef; -} - -export interface RowTypedQuery { - readonly [QueryBrand]: true; - // Phantom type to track the row type. - readonly __row?: Row; - readonly __algebraicType?: ST; -} - -export type Query = RowTypedQuery< - RowType, - TableDef['rowType'] ->; - -export const isRowTypedQuery = (val: unknown): val is RowTypedQuery => - !!val && typeof val === 'object' && QueryBrand in (val as object); - -export const isTypedQuery = (val: unknown): val is TableTypedQuery => - !!val && typeof val === 'object' && QueryBrand in (val as object); - -export function toSql(q: Query): string { - return (q as unknown as { toSql(): string }).toSql(); -} - -// A query builder with a single table. -type From = Readonly<{ - where( - predicate: (row: RowExpr) => BooleanExpr - ): From; - rightSemijoin( - other: TableRef, - on: ( - left: IndexedRowExpr, - right: IndexedRowExpr - ) => EqExpr - ): SemijoinBuilder; - leftSemijoin( - other: TableRef, - on: ( - left: IndexedRowExpr, - right: IndexedRowExpr - ) => EqExpr - ): SemijoinBuilder; - build(): Query; -}>; - -// A query builder with a semijoin. -type SemijoinBuilder = Readonly<{ - where( - predicate: (row: RowExpr) => BooleanExpr - ): SemijoinBuilder; - build(): Query; -}>; - -class SemijoinImpl - implements SemijoinBuilder, TableTypedQuery -{ - readonly [QueryBrand] = true; - readonly type = 'semijoin' as const; - constructor( - readonly sourceQuery: FromBuilder, - readonly filterQuery: FromBuilder, - readonly joinCondition: EqExpr - ) { - if (sourceQuery.table.name === filterQuery.table.name) { - // TODO: Handle aliasing properly instead of just forbidding it. - throw new Error('Cannot semijoin a table to itself'); - } - } - - build(): Query { - return this as Query; - } - - where( - predicate: (row: RowExpr) => BooleanExpr - ): SemijoinImpl { - const nextSourceQuery = this.sourceQuery.where(predicate); - return new SemijoinImpl( - nextSourceQuery, - this.filterQuery, - this.joinCondition - ); - } - - toSql(): string { - const left = this.filterQuery; - const right = this.sourceQuery; - const leftTable = quoteIdentifier(left.table.name); - const rightTable = quoteIdentifier(right.table.name); - let sql = `SELECT ${rightTable}.* FROM ${leftTable} JOIN ${rightTable} ON ${booleanExprToSql(this.joinCondition)}`; - - const clauses: string[] = []; - if (left.whereClause) { - clauses.push(booleanExprToSql(left.whereClause)); - } - if (right.whereClause) { - clauses.push(booleanExprToSql(right.whereClause)); - } - - if (clauses.length > 0) { - const whereSql = - clauses.length === 1 - ? clauses[0] - : clauses.map(wrapInParens).join(' AND '); - sql += ` WHERE ${whereSql}`; - } - - return sql; - } -} - -class FromBuilder - implements From, TableTypedQuery -{ - readonly [QueryBrand] = true; - constructor( - readonly table: TableRef, - readonly whereClause?: BooleanExpr - ) {} - - where( - predicate: (row: RowExpr) => BooleanExpr - ): FromBuilder { - const newCondition = predicate(this.table.cols); - const nextWhere = this.whereClause - ? and(this.whereClause, newCondition) - : newCondition; - return new FromBuilder(this.table, nextWhere); - } - - rightSemijoin( - right: TableRef, - on: ( - left: IndexedRowExpr, - right: IndexedRowExpr - ) => EqExpr - ): SemijoinBuilder { - const sourceQuery = new FromBuilder(right); - const joinCondition = on( - this.table.indexedCols, - right.indexedCols - ) as EqExpr; - return new SemijoinImpl(sourceQuery, this, joinCondition); - } - - leftSemijoin( - right: TableRef, - on: ( - left: IndexedRowExpr, - right: IndexedRowExpr - ) => EqExpr - ): SemijoinBuilder { - const filterQuery = new FromBuilder(right); - const joinCondition = on( - this.table.indexedCols, - right.indexedCols - ) as EqExpr; - return new SemijoinImpl(this, filterQuery, joinCondition); - } - - toSql(): string { - return renderSelectSqlWithJoins(this.table, this.whereClause); - } - - build(): Query { - return this as Query; - } -} - -export type QueryBuilder = { - readonly [Tbl in SchemaDef['tables'][number] as Tbl['name']]: TableRef & - From; -} & {}; - -/** - * A runtime reference to a table. This materializes the RowExpr for us. - * TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work. - */ -export type TableRef = Readonly<{ - type: 'table'; - name: TableDef['name']; - cols: RowExpr; - indexedCols: IndexedRowExpr; - // Maybe redundant. - tableDef: TableDef; -}>; - -class TableRefImpl - implements TableRef, From -{ - readonly type = 'table' as const; - name: string; - cols: RowExpr; - indexedCols: IndexedRowExpr; - tableDef: TableDef; - constructor(tableDef: TableDef) { - this.name = tableDef.name; - this.cols = createRowExpr(tableDef); - // this.indexedCols = createIndexedRowExpr(tableDef, this.cols); - // TODO: we could create an indexedRowExpr to avoid having the extra columns. - // Right now, the objects we pass will actually have all the columns, but the - // type system will consider it an error. - this.indexedCols = this.cols; - this.tableDef = tableDef; - Object.freeze(this); - } - - asFrom(): FromBuilder { - return new FromBuilder(this); - } - - rightSemijoin( - other: TableRef, - on: ( - left: IndexedRowExpr, - right: IndexedRowExpr - ) => EqExpr - ): SemijoinBuilder { - return this.asFrom().rightSemijoin(other, on); - } - - leftSemijoin( - other: TableRef, - on: ( - left: IndexedRowExpr, - right: IndexedRowExpr - ) => EqExpr - ): SemijoinBuilder { - return this.asFrom().leftSemijoin(other, on); - } - - build(): Query { - return this.asFrom().build(); - } - - toSql(): string { - return this.asFrom().toSql(); - } - - where( - predicate: (row: RowExpr) => BooleanExpr - ): FromBuilder { - return this.asFrom().where(predicate); - } -} - -export type RefSource = - | TableRef - | { ref(): TableRef }; - -export function createTableRefFromDef( - tableDef: TableDef -): TableRef { - return new TableRefImpl(tableDef); -} - -export function makeQueryBuilder( - schema: SchemaDef -): QueryBuilder { - const qb = Object.create(null) as QueryBuilder; - for (const table of schema.tables) { - const ref = createTableRefFromDef( - table as TableDefByName> - ); - (qb as Record>)[table.name] = ref; - } - return Object.freeze(qb) as QueryBuilder; -} - -function createRowExpr( - tableDef: TableDef -): RowExpr { - const row: Record> = {}; - for (const columnName of Object.keys(tableDef.columns) as Array< - keyof TableDef['columns'] & string - >) { - const columnBuilder = tableDef.columns[columnName]; - const column = new ColumnExpression( - tableDef.name, - columnName, - columnBuilder.typeBuilder.algebraicType as InferSpacetimeTypeOfColumn< - TableDef, - typeof columnName - > - ); - row[columnName] = Object.freeze(column); - } - return Object.freeze(row) as RowExpr; -} - -export function from( - source: RefSource -): From { - return new FromBuilder(resolveTableRef(source)); -} - -function resolveTableRef( - source: RefSource -): TableRef { - if (typeof (source as { ref?: unknown }).ref === 'function') { - return (source as { ref(): TableRef }).ref(); - } - return source as TableRef; -} - -function renderSelectSqlWithJoins
( - table: TableRef
, - where?: BooleanExpr
, - extraClauses: readonly string[] = [] -): string { - const quotedTable = quoteIdentifier(table.name); - const sql = `SELECT * FROM ${quotedTable}`; - const clauses: string[] = []; - if (where) clauses.push(booleanExprToSql(where)); - clauses.push(...extraClauses); - if (clauses.length === 0) return sql; - const whereSql = - clauses.length === 1 ? clauses[0] : clauses.map(wrapInParens).join(' AND '); - return `${sql} WHERE ${whereSql}`; -} - -// TODO: Just use UntypedTableDef if they end up being the same. -export type TypedTableDef< - Columns extends Record< - string, - ColumnBuilder> - > = Record>>, -> = { - name: string; - columns: Columns; - indexes: readonly IndexOpts[]; - rowType: RowBuilder['algebraicType']['value']; -}; - -export type TableSchemaAsTableDef< - TSchema extends TableSchema, -> = { - name: TSchema['tableName']; - columns: TSchema['rowType']['row']; - indexes: TSchema['idxs']; -}; - -type RowType = { - [K in keyof TableDef['columns']]: TableDef['columns'][K] extends ColumnBuilder< - infer T, - any, - any - > - ? T - : never; -}; - -// TODO: Consider making a smaller version of these types that doesn't expose the internals. -// Restricting it later should not break anyone in practice. -export type ColumnExpr< - TableDef extends TypedTableDef, - ColumnName extends ColumnNames, -> = ColumnExpression; - -type ColumnSpacetimeType> = - Col extends ColumnExpr - ? InferSpacetimeTypeOfColumn - : never; - -// TODO: This checks that they match, but we also need to make sure that they are comparable types, -// since you can use product types at all. -type ColumnSameSpacetime< - ThisTable extends TypedTableDef, - ThisCol extends ColumnNames, - OtherCol extends ColumnExpr, -> = [InferSpacetimeTypeOfColumn] extends [ - ColumnSpacetimeType, -] - ? [ColumnSpacetimeType] extends [ - InferSpacetimeTypeOfColumn, - ] - ? OtherCol - : never - : never; - -// Helper to get the table back from a column. -type ExtractTable> = - Col extends ColumnExpr ? T : never; - -export class ColumnExpression< - TableDef extends TypedTableDef, - ColumnName extends ColumnNames, -> { - readonly type = 'column' as const; - readonly column: ColumnName; - readonly table: TableDef['name']; - // phantom: actual runtime value is undefined - readonly tsValueType?: RowType[ColumnName]; - readonly spacetimeType: InferSpacetimeTypeOfColumn; - - constructor( - table: TableDef['name'], - column: ColumnName, - spacetimeType: InferSpacetimeTypeOfColumn - ) { - this.table = table; - this.column = column; - this.spacetimeType = spacetimeType; - } - - eq(literal: LiteralValue & RowType[ColumnName]): EqExpr; - eq>( - value: ColumnSameSpacetime - ): EqExpr>; - - // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. - eq(x: any): any { - return { - type: 'eq', - left: this as unknown as ValueExpr, - right: normalizeValue(x) as ValueExpr, - } as EqExpr; - } - - lt( - literal: LiteralValue & RowType[ColumnName] - ): BooleanExpr; - lt>( - value: ColumnSameSpacetime - ): BooleanExpr>; - - // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. - lt(x: any): any { - return { - type: 'lt', - left: this as unknown as ValueExpr, - right: normalizeValue(x) as ValueExpr, - } as BooleanExpr; - } - lte( - literal: LiteralValue & RowType[ColumnName] - ): BooleanExpr; - lte>( - value: ColumnSameSpacetime - ): BooleanExpr>; - - // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. - lte(x: any): any { - return { - type: 'lte', - left: this as unknown as ValueExpr, - right: normalizeValue(x) as ValueExpr, - } as BooleanExpr; - } - - gt( - literal: LiteralValue & RowType[ColumnName] - ): BooleanExpr; - gt>( - value: ColumnSameSpacetime - ): BooleanExpr>; - - // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. - gt(x: any): any { - return { - type: 'gt', - left: this as unknown as ValueExpr, - right: normalizeValue(x) as ValueExpr, - } as BooleanExpr; - } - gte( - literal: LiteralValue & RowType[ColumnName] - ): BooleanExpr; - gte>( - value: ColumnSameSpacetime - ): BooleanExpr>; - - // These types could be tighted, but since we declare the overloads above, it doesn't weaken the API surface. - gte(x: any): any { - return { - type: 'gte', - left: this as unknown as ValueExpr, - right: normalizeValue(x) as ValueExpr, - } as BooleanExpr; - } -} - -/** - * Helper to get the spacetime type of a column. - */ -type InferSpacetimeTypeOfColumn< - TableDef extends TypedTableDef, - ColumnName extends ColumnNames, -> = - TableDef['columns'][ColumnName]['typeBuilder'] extends TypeBuilder< - any, - infer U - > - ? U - : never; - -type ColumnNames = keyof RowType & - string; - -// For composite indexes, we only consider it as an index over the first column in the index. -type FirstIndexColumn> = - IndexColumns extends readonly [infer Head extends string, ...infer _Rest] - ? Head - : never; - -// Columns that are indexed by something in the indexes: [...] part. -type ExplicitIndexedColumns = - TableDef['indexes'][number] extends infer I - ? I extends IndexOpts> - ? FirstIndexColumn & ColumnNames - : never - : never; - -// Columns with an index defined on the column definition. -type MetadataIndexedColumns = { - [K in ColumnNames]: ColumnIndex< - K, - TableDef['columns'][K]['columnMetadata'] - > extends never - ? never - : K; -}[ColumnNames]; - -export type IndexedColumnNames = - | ExplicitIndexedColumns - | MetadataIndexedColumns; - -export type IndexedRowExpr = Readonly<{ - readonly [C in IndexedColumnNames]: ColumnExpr; -}>; - -/** - * Acts as a row when writing filters for queries. It is a way to get column references. - */ -export type RowExpr = Readonly<{ - readonly [C in ColumnNames]: ColumnExpr; -}>; - -/** - * Union of ColumnExprs from Table whose spacetimeType is compatible with Value - * (produces a union of ColumnExpr for matching columns). - */ -export type ColumnExprForValue
= { - [C in ColumnNames
]: InferSpacetimeTypeOfColumn extends Value - ? ColumnExpr - : never; -}[ColumnNames
]; - -type LiteralValue = - | string - | number - | bigint - | boolean - | Identity - | ConnectionId; - -type ValueLike = LiteralValue | ColumnExpr | LiteralExpr; -type ValueInput = - | ValueLike - | ValueExpr; - -export type ValueExpr = - | LiteralExpr - | ColumnExprForValue; - -type LiteralExpr = { - type: 'literal'; - value: Value; -}; - -export function literal( - value: Value -): ValueExpr { - return { type: 'literal', value }; -} - -// This is here to take literal values and wrap them in an AST node. -function normalizeValue(val: ValueInput): ValueExpr { - if ((val as LiteralExpr).type === 'literal') - return val as LiteralExpr; - if ( - typeof val === 'object' && - val != null && - 'type' in (val as any) && - (val as any).type === 'column' - ) { - return val as ColumnExpr; - } - return literal(val as LiteralValue); -} - -type EqExpr
= { - type: 'eq'; - left: ValueExpr; - right: ValueExpr; -} & { - _tableType?: Table; -}; - -type BooleanExpr
= ( - | { - type: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte'; - left: ValueExpr; - right: ValueExpr; - } - | { - type: 'and'; - clauses: readonly [ - BooleanExpr
, - BooleanExpr
, - ...BooleanExpr
[], - ]; - } - | { - type: 'or'; - clauses: readonly [ - BooleanExpr
, - BooleanExpr
, - ...BooleanExpr
[], - ]; - } - | { - type: 'not'; - clause: BooleanExpr
; - } -) & { - _tableType?: Table; - // readonly [BooleanExprBrand]: Table?; -}; - -export function not( - clause: BooleanExpr -): BooleanExpr { - return { type: 'not', clause }; -} - -export function and( - ...clauses: readonly [BooleanExpr, BooleanExpr, ...BooleanExpr[]] -): BooleanExpr { - return { type: 'and', clauses }; -} - -export function or( - ...clauses: readonly [BooleanExpr, BooleanExpr, ...BooleanExpr[]] -): BooleanExpr { - return { type: 'or', clauses }; -} - -function booleanExprToSql
( - expr: BooleanExpr
, - tableAlias?: string -): string { - switch (expr.type) { - case 'eq': - return `${valueExprToSql(expr.left, tableAlias)} = ${valueExprToSql(expr.right, tableAlias)}`; - case 'ne': - return `${valueExprToSql(expr.left, tableAlias)} <> ${valueExprToSql(expr.right, tableAlias)}`; - case 'gt': - return `${valueExprToSql(expr.left, tableAlias)} > ${valueExprToSql(expr.right, tableAlias)}`; - case 'gte': - return `${valueExprToSql(expr.left, tableAlias)} >= ${valueExprToSql(expr.right, tableAlias)}`; - case 'lt': - return `${valueExprToSql(expr.left, tableAlias)} < ${valueExprToSql(expr.right, tableAlias)}`; - case 'lte': - return `${valueExprToSql(expr.left, tableAlias)} <= ${valueExprToSql(expr.right, tableAlias)}`; - case 'and': - return expr.clauses - .map(c => booleanExprToSql(c, tableAlias)) - .map(wrapInParens) - .join(' AND '); - case 'or': - return expr.clauses - .map(c => booleanExprToSql(c, tableAlias)) - .map(wrapInParens) - .join(' OR '); - case 'not': - return `NOT ${wrapInParens(booleanExprToSql(expr.clause, tableAlias))}`; - } -} - -function wrapInParens(sql: string): string { - return `(${sql})`; -} - -function valueExprToSql
( - expr: ValueExpr, - tableAlias?: string -): string { - if (isLiteralExpr(expr)) { - return literalValueToSql(expr.value); - } - const table = tableAlias ?? expr.table; - return `${quoteIdentifier(table)}.${quoteIdentifier(expr.column)}`; -} - -function literalValueToSql(value: unknown): string { - if (value === null || value === undefined) { - return 'NULL'; - } - if (value instanceof Identity || value instanceof ConnectionId) { - // We use this hex string syntax. - return `0x${value.toHexString()}`; - } - switch (typeof value) { - case 'number': - case 'bigint': - return String(value); - case 'boolean': - return value ? 'TRUE' : 'FALSE'; - case 'string': - return `'${value.replace(/'/g, "''")}'`; - default: - // It might be safer to error here? - return `'${JSON.stringify(value).replace(/'/g, "''")}'`; - } -} - -function quoteIdentifier(name: string): string { - return `"${name.replace(/"/g, '""')}"`; -} - -function isLiteralExpr( - expr: ValueExpr -): expr is LiteralExpr { - return (expr as LiteralExpr).type === 'literal'; -} - -// TODO: Fix this. -function _createIndexedRowExpr( - tableDef: TableDef, - cols: RowExpr -): IndexedRowExpr { - const indexed = new Set>(); - for (const idx of tableDef.indexes) { - if ('columns' in idx) { - const [first] = idx.columns; - if (first) indexed.add(first); - } else if ('column' in idx) { - indexed.add(idx.column); - } - } - const pickedEntries = [...indexed].map(name => [name, cols[name]]); - return Object.freeze( - Object.fromEntries(pickedEntries) - ) as IndexedRowExpr; -} +export * from '../lib/query'; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-app/src/module_bindings/index.ts index 46ef1b99735..b12362ccf8a 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.0 (commit 492e591845db8b174ee885b74294cb4ecbf655dc). +// This was generated using spacetimedb cli version 1.11.2 (commit 902af09c55b418c987000e739eb176a3368296ca). /* eslint-disable */ /* tslint:disable */ @@ -10,7 +10,9 @@ import { DbConnectionImpl as __DbConnectionImpl, SubscriptionBuilderImpl as __SubscriptionBuilderImpl, TypeBuilder as __TypeBuilder, + Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -109,7 +111,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '1.11.0' as const, + cliVersion: '1.11.2' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, @@ -123,6 +125,9 @@ const REMOTE_MODULE = { /** The tables available in this remote SpacetimeDB module. */ export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); +/** A typed query builder for this remote SpacetimeDB module. */ +export const query = __makeQueryBuilder(tablesSchema.schemaType); + /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( reducersSchema.reducersType.reducers diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 99d802a454c..0a25c2646dd 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -348,6 +348,12 @@ impl Lang for TypeScript { "export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables);" ); writeln!(out); + writeln!(out, "/** A typed query builder for this remote SpacetimeDB module. */"); + writeln!( + out, + "export const query = __makeQueryBuilder(tablesSchema.schemaType);" + ); + writeln!(out); writeln!(out, "/** The reducers available in this remote SpacetimeDB module. */"); writeln!( out, @@ -457,6 +463,7 @@ fn print_index_imports(out: &mut Indenter) { "Uuid as __Uuid", "DbConnectionBuilder as __DbConnectionBuilder", "convertToAccessorMap as __convertToAccessorMap", + "makeQueryBuilder as __makeQueryBuilder", "type EventContextInterface as __EventContextInterface", "type ReducerEventContextInterface as __ReducerEventContextInterface", "type SubscriptionEventContextInterface as __SubscriptionEventContextInterface", diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index fb50aa0de68..7fb75d915c2 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -233,6 +233,7 @@ import { TypeBuilder as __TypeBuilder, Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -559,6 +560,9 @@ const REMOTE_MODULE = { /** The tables available in this remote SpacetimeDB module. */ export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); +/** A typed query builder for this remote SpacetimeDB module. */ +export const query = __makeQueryBuilder(tablesSchema.schemaType); + /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); diff --git a/templates/basic-react/src/module_bindings/add_reducer.ts b/templates/basic-react/src/module_bindings/add_reducer.ts index ce493ee8574..85081559c7d 100644 --- a/templates/basic-react/src/module_bindings/add_reducer.ts +++ b/templates/basic-react/src/module_bindings/add_reducer.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default { name: __t.string(), diff --git a/templates/basic-react/src/module_bindings/index.ts b/templates/basic-react/src/module_bindings/index.ts index 6d610f99e69..286aa9c45ba 100644 --- a/templates/basic-react/src/module_bindings/index.ts +++ b/templates/basic-react/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.2 (commit dc5997d48f7c472faf2756b07c012bcf28edc50b). +// This was generated using spacetimedb cli version 1.11.2 (commit 902af09c55b418c987000e739eb176a3368296ca). /* eslint-disable */ /* tslint:disable */ @@ -12,6 +12,7 @@ import { TypeBuilder as __TypeBuilder, Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -29,53 +30,63 @@ import { type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, type SubscriptionHandleImpl as __SubscriptionHandleImpl, -} from "spacetimedb"; +} from 'spacetimedb'; // Import and reexport all reducer arg types -import OnConnectReducer from "./on_connect_reducer"; +import OnConnectReducer from './on_connect_reducer'; export { OnConnectReducer }; -import OnDisconnectReducer from "./on_disconnect_reducer"; +import OnDisconnectReducer from './on_disconnect_reducer'; export { OnDisconnectReducer }; -import AddReducer from "./add_reducer"; +import AddReducer from './add_reducer'; export { AddReducer }; -import SayHelloReducer from "./say_hello_reducer"; +import SayHelloReducer from './say_hello_reducer'; export { SayHelloReducer }; // Import and reexport all procedure arg types // Import and reexport all table handle types -import PersonRow from "./person_table"; +import PersonRow from './person_table'; export { PersonRow }; // Import and reexport all types -import Person from "./person_type"; +import Add from './add_type'; +export { Add }; +import Init from './init_type'; +export { Init }; +import OnConnect from './on_connect_type'; +export { OnConnect }; +import OnDisconnect from './on_disconnect_type'; +export { OnDisconnect }; +import Person from './person_type'; export { Person }; +import SayHello from './say_hello_type'; +export { SayHello }; /** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ const tablesSchema = __schema( - __table({ - name: 'person', - indexes: [ - ], - constraints: [ - ], - }, PersonRow), + __table( + { + name: 'person', + indexes: [], + constraints: [], + }, + PersonRow + ) ); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ const reducersSchema = __reducers( - __reducerSchema("add", AddReducer), - __reducerSchema("say_hello", SayHelloReducer), + __reducerSchema('add', AddReducer), + __reducerSchema('say_hello', SayHelloReducer) ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ -const proceduresSchema = __procedures( -); +const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: "1.11.2" as const, + cliVersion: '1.11.2' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, @@ -89,22 +100,33 @@ const REMOTE_MODULE = { /** The tables available in this remote SpacetimeDB module. */ export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); +/** A typed query builder for this remote SpacetimeDB module. */ +export const query = __makeQueryBuilder(tablesSchema.schemaType); + /** The reducers available in this remote SpacetimeDB module. */ -export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); +export const reducers = __convertToAccessorMap( + reducersSchema.reducersType.reducers +); /** The context type returned in callbacks for all possible events. */ export type EventContext = __EventContextInterface; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; +export type ReducerEventContext = __ReducerEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; +export type SubscriptionEventContext = __SubscriptionEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for error events. */ export type ErrorContext = __ErrorContextInterface; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; /** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ -export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} +export class SubscriptionBuilder extends __SubscriptionBuilderImpl< + typeof REMOTE_MODULE +> {} /** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ export class DbConnectionBuilder extends __DbConnectionBuilder {} @@ -113,7 +135,11 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} export class DbConnection extends __DbConnectionImpl { /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { - return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); + return new DbConnectionBuilder( + REMOTE_MODULE, + (config: __DbConnectionConfig) => + new DbConnection(config) + ); }; /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ @@ -121,4 +147,3 @@ export class DbConnection extends __DbConnectionImpl { return new SubscriptionBuilder(this); }; } - diff --git a/templates/basic-react/src/module_bindings/on_connect_reducer.ts b/templates/basic-react/src/module_bindings/on_connect_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/basic-react/src/module_bindings/on_connect_reducer.ts +++ b/templates/basic-react/src/module_bindings/on_connect_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/basic-react/src/module_bindings/on_disconnect_reducer.ts b/templates/basic-react/src/module_bindings/on_disconnect_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/basic-react/src/module_bindings/on_disconnect_reducer.ts +++ b/templates/basic-react/src/module_bindings/on_disconnect_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/basic-react/src/module_bindings/person_table.ts b/templates/basic-react/src/module_bindings/person_table.ts index 4dc4a822cc3..0f70f74f617 100644 --- a/templates/basic-react/src/module_bindings/person_table.ts +++ b/templates/basic-react/src/module_bindings/person_table.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default __t.row({ name: __t.string(), diff --git a/templates/basic-react/src/module_bindings/person_type.ts b/templates/basic-react/src/module_bindings/person_type.ts index 817473e2817..1156775a3cf 100644 --- a/templates/basic-react/src/module_bindings/person_type.ts +++ b/templates/basic-react/src/module_bindings/person_type.ts @@ -8,10 +8,8 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; -export default __t.object("Person", { +export default __t.object('Person', { name: __t.string(), }); - - diff --git a/templates/basic-react/src/module_bindings/say_hello_reducer.ts b/templates/basic-react/src/module_bindings/say_hello_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/basic-react/src/module_bindings/say_hello_reducer.ts +++ b/templates/basic-react/src/module_bindings/say_hello_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/basic-typescript/src/module_bindings/add_reducer.ts b/templates/basic-typescript/src/module_bindings/add_reducer.ts index ce493ee8574..85081559c7d 100644 --- a/templates/basic-typescript/src/module_bindings/add_reducer.ts +++ b/templates/basic-typescript/src/module_bindings/add_reducer.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default { name: __t.string(), diff --git a/templates/basic-typescript/src/module_bindings/index.ts b/templates/basic-typescript/src/module_bindings/index.ts index 6d610f99e69..286aa9c45ba 100644 --- a/templates/basic-typescript/src/module_bindings/index.ts +++ b/templates/basic-typescript/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.2 (commit dc5997d48f7c472faf2756b07c012bcf28edc50b). +// This was generated using spacetimedb cli version 1.11.2 (commit 902af09c55b418c987000e739eb176a3368296ca). /* eslint-disable */ /* tslint:disable */ @@ -12,6 +12,7 @@ import { TypeBuilder as __TypeBuilder, Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -29,53 +30,63 @@ import { type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, type SubscriptionHandleImpl as __SubscriptionHandleImpl, -} from "spacetimedb"; +} from 'spacetimedb'; // Import and reexport all reducer arg types -import OnConnectReducer from "./on_connect_reducer"; +import OnConnectReducer from './on_connect_reducer'; export { OnConnectReducer }; -import OnDisconnectReducer from "./on_disconnect_reducer"; +import OnDisconnectReducer from './on_disconnect_reducer'; export { OnDisconnectReducer }; -import AddReducer from "./add_reducer"; +import AddReducer from './add_reducer'; export { AddReducer }; -import SayHelloReducer from "./say_hello_reducer"; +import SayHelloReducer from './say_hello_reducer'; export { SayHelloReducer }; // Import and reexport all procedure arg types // Import and reexport all table handle types -import PersonRow from "./person_table"; +import PersonRow from './person_table'; export { PersonRow }; // Import and reexport all types -import Person from "./person_type"; +import Add from './add_type'; +export { Add }; +import Init from './init_type'; +export { Init }; +import OnConnect from './on_connect_type'; +export { OnConnect }; +import OnDisconnect from './on_disconnect_type'; +export { OnDisconnect }; +import Person from './person_type'; export { Person }; +import SayHello from './say_hello_type'; +export { SayHello }; /** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ const tablesSchema = __schema( - __table({ - name: 'person', - indexes: [ - ], - constraints: [ - ], - }, PersonRow), + __table( + { + name: 'person', + indexes: [], + constraints: [], + }, + PersonRow + ) ); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ const reducersSchema = __reducers( - __reducerSchema("add", AddReducer), - __reducerSchema("say_hello", SayHelloReducer), + __reducerSchema('add', AddReducer), + __reducerSchema('say_hello', SayHelloReducer) ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ -const proceduresSchema = __procedures( -); +const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: "1.11.2" as const, + cliVersion: '1.11.2' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, @@ -89,22 +100,33 @@ const REMOTE_MODULE = { /** The tables available in this remote SpacetimeDB module. */ export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); +/** A typed query builder for this remote SpacetimeDB module. */ +export const query = __makeQueryBuilder(tablesSchema.schemaType); + /** The reducers available in this remote SpacetimeDB module. */ -export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); +export const reducers = __convertToAccessorMap( + reducersSchema.reducersType.reducers +); /** The context type returned in callbacks for all possible events. */ export type EventContext = __EventContextInterface; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; +export type ReducerEventContext = __ReducerEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; +export type SubscriptionEventContext = __SubscriptionEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for error events. */ export type ErrorContext = __ErrorContextInterface; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; /** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ -export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} +export class SubscriptionBuilder extends __SubscriptionBuilderImpl< + typeof REMOTE_MODULE +> {} /** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ export class DbConnectionBuilder extends __DbConnectionBuilder {} @@ -113,7 +135,11 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} export class DbConnection extends __DbConnectionImpl { /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { - return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); + return new DbConnectionBuilder( + REMOTE_MODULE, + (config: __DbConnectionConfig) => + new DbConnection(config) + ); }; /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ @@ -121,4 +147,3 @@ export class DbConnection extends __DbConnectionImpl { return new SubscriptionBuilder(this); }; } - diff --git a/templates/basic-typescript/src/module_bindings/on_connect_reducer.ts b/templates/basic-typescript/src/module_bindings/on_connect_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/basic-typescript/src/module_bindings/on_connect_reducer.ts +++ b/templates/basic-typescript/src/module_bindings/on_connect_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/basic-typescript/src/module_bindings/on_disconnect_reducer.ts b/templates/basic-typescript/src/module_bindings/on_disconnect_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/basic-typescript/src/module_bindings/on_disconnect_reducer.ts +++ b/templates/basic-typescript/src/module_bindings/on_disconnect_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/basic-typescript/src/module_bindings/person_table.ts b/templates/basic-typescript/src/module_bindings/person_table.ts index 4dc4a822cc3..0f70f74f617 100644 --- a/templates/basic-typescript/src/module_bindings/person_table.ts +++ b/templates/basic-typescript/src/module_bindings/person_table.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default __t.row({ name: __t.string(), diff --git a/templates/basic-typescript/src/module_bindings/person_type.ts b/templates/basic-typescript/src/module_bindings/person_type.ts index 817473e2817..1156775a3cf 100644 --- a/templates/basic-typescript/src/module_bindings/person_type.ts +++ b/templates/basic-typescript/src/module_bindings/person_type.ts @@ -8,10 +8,8 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; -export default __t.object("Person", { +export default __t.object('Person', { name: __t.string(), }); - - diff --git a/templates/basic-typescript/src/module_bindings/say_hello_reducer.ts b/templates/basic-typescript/src/module_bindings/say_hello_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/basic-typescript/src/module_bindings/say_hello_reducer.ts +++ b/templates/basic-typescript/src/module_bindings/say_hello_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/quickstart-chat-typescript/src/module_bindings/index.ts b/templates/quickstart-chat-typescript/src/module_bindings/index.ts index 42cbd9d5a06..aa58e322133 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/index.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.2 (commit dc5997d48f7c472faf2756b07c012bcf28edc50b). +// This was generated using spacetimedb cli version 1.11.2 (commit 902af09c55b418c987000e739eb176a3368296ca). /* eslint-disable */ /* tslint:disable */ @@ -12,6 +12,7 @@ import { TypeBuilder as __TypeBuilder, Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -29,78 +30,83 @@ import { type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, type SubscriptionHandleImpl as __SubscriptionHandleImpl, -} from "spacetimedb"; +} from 'spacetimedb'; // Import and reexport all reducer arg types -import SetNameReducer from "./set_name_reducer"; +import SetNameReducer from './set_name_reducer'; export { SetNameReducer }; -import SendMessageReducer from "./send_message_reducer"; +import SendMessageReducer from './send_message_reducer'; export { SendMessageReducer }; -import OnConnectReducer from "./on_connect_reducer"; +import OnConnectReducer from './on_connect_reducer'; export { OnConnectReducer }; -import OnDisconnectReducer from "./on_disconnect_reducer"; +import OnDisconnectReducer from './on_disconnect_reducer'; export { OnDisconnectReducer }; // Import and reexport all procedure arg types // Import and reexport all table handle types -import MessageRow from "./message_table"; +import MessageRow from './message_table'; export { MessageRow }; -import UserRow from "./user_table"; +import UserRow from './user_table'; export { UserRow }; // Import and reexport all types -import Init from "./init_type"; +import Init from './init_type'; export { Init }; -import Message from "./message_type"; +import Message from './message_type'; export { Message }; -import OnConnect from "./on_connect_type"; +import OnConnect from './on_connect_type'; export { OnConnect }; -import OnDisconnect from "./on_disconnect_type"; +import OnDisconnect from './on_disconnect_type'; export { OnDisconnect }; -import SendMessage from "./send_message_type"; +import SendMessage from './send_message_type'; export { SendMessage }; -import SetName from "./set_name_type"; +import SetName from './set_name_type'; export { SetName }; -import User from "./user_type"; +import User from './user_type'; export { User }; /** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ const tablesSchema = __schema( - __table({ - name: 'message', - indexes: [ - ], - constraints: [ - ], - }, MessageRow), - __table({ - name: 'user', - indexes: [ - { name: 'identity', algorithm: 'btree', columns: [ - 'identity', - ] }, - ], - constraints: [ - { name: 'user_identity_key', constraint: 'unique', columns: ['identity'] }, - ], - }, UserRow), + __table( + { + name: 'message', + indexes: [], + constraints: [], + }, + MessageRow + ), + __table( + { + name: 'user', + indexes: [ + { name: 'identity', algorithm: 'btree', columns: ['identity'] }, + ], + constraints: [ + { + name: 'user_identity_key', + constraint: 'unique', + columns: ['identity'], + }, + ], + }, + UserRow + ) ); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ const reducersSchema = __reducers( - __reducerSchema("set_name", SetNameReducer), - __reducerSchema("send_message", SendMessageReducer), + __reducerSchema('set_name', SetNameReducer), + __reducerSchema('send_message', SendMessageReducer) ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ -const proceduresSchema = __procedures( -); +const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: "1.11.2" as const, + cliVersion: '1.11.2' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, @@ -114,22 +120,33 @@ const REMOTE_MODULE = { /** The tables available in this remote SpacetimeDB module. */ export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); +/** A typed query builder for this remote SpacetimeDB module. */ +export const query = __makeQueryBuilder(tablesSchema.schemaType); + /** The reducers available in this remote SpacetimeDB module. */ -export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); +export const reducers = __convertToAccessorMap( + reducersSchema.reducersType.reducers +); /** The context type returned in callbacks for all possible events. */ export type EventContext = __EventContextInterface; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; +export type ReducerEventContext = __ReducerEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; +export type SubscriptionEventContext = __SubscriptionEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for error events. */ export type ErrorContext = __ErrorContextInterface; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; /** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ -export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} +export class SubscriptionBuilder extends __SubscriptionBuilderImpl< + typeof REMOTE_MODULE +> {} /** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ export class DbConnectionBuilder extends __DbConnectionBuilder {} @@ -138,7 +155,11 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} export class DbConnection extends __DbConnectionImpl { /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { - return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); + return new DbConnectionBuilder( + REMOTE_MODULE, + (config: __DbConnectionConfig) => + new DbConnection(config) + ); }; /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ @@ -146,4 +167,3 @@ export class DbConnection extends __DbConnectionImpl { return new SubscriptionBuilder(this); }; } - diff --git a/templates/quickstart-chat-typescript/src/module_bindings/init_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/init_type.ts index 847f94de0ec..52ed691ed94 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/init_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/init_type.ts @@ -8,8 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; - -export default __t.object("Init", {}); - +} from 'spacetimedb'; +export default __t.object('Init', {}); diff --git a/templates/quickstart-chat-typescript/src/module_bindings/message_table.ts b/templates/quickstart-chat-typescript/src/module_bindings/message_table.ts index 2f08057cede..87044c64df4 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/message_table.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/message_table.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default __t.row({ sender: __t.identity(), diff --git a/templates/quickstart-chat-typescript/src/module_bindings/message_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/message_type.ts index d3b0e9dde03..c15fedf0f6a 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/message_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/message_type.ts @@ -8,12 +8,10 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; -export default __t.object("Message", { +export default __t.object('Message', { sender: __t.identity(), sent: __t.timestamp(), text: __t.string(), }); - - diff --git a/templates/quickstart-chat-typescript/src/module_bindings/on_connect_reducer.ts b/templates/quickstart-chat-typescript/src/module_bindings/on_connect_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/on_connect_reducer.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/on_connect_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/quickstart-chat-typescript/src/module_bindings/on_connect_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/on_connect_type.ts index d95ba1fa6e2..d36362515de 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/on_connect_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/on_connect_type.ts @@ -8,8 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; - -export default __t.object("OnConnect", {}); - +} from 'spacetimedb'; +export default __t.object('OnConnect', {}); diff --git a/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_reducer.ts b/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_reducer.ts index e18fbc0a086..2ca99c88fea 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_reducer.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_reducer.ts @@ -8,6 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default {}; diff --git a/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_type.ts index 3d29234b70e..efda71ebcfd 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/on_disconnect_type.ts @@ -8,8 +8,6 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; - -export default __t.object("OnDisconnect", {}); - +} from 'spacetimedb'; +export default __t.object('OnDisconnect', {}); diff --git a/templates/quickstart-chat-typescript/src/module_bindings/send_message_reducer.ts b/templates/quickstart-chat-typescript/src/module_bindings/send_message_reducer.ts index 0039b8ebcdc..4aeb65a0ae9 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/send_message_reducer.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/send_message_reducer.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default { text: __t.string(), diff --git a/templates/quickstart-chat-typescript/src/module_bindings/send_message_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/send_message_type.ts index 612f7336270..7414d114886 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/send_message_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/send_message_type.ts @@ -8,10 +8,8 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; -export default __t.object("SendMessage", { +export default __t.object('SendMessage', { text: __t.string(), }); - - diff --git a/templates/quickstart-chat-typescript/src/module_bindings/set_name_reducer.ts b/templates/quickstart-chat-typescript/src/module_bindings/set_name_reducer.ts index ce493ee8574..85081559c7d 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/set_name_reducer.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/set_name_reducer.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default { name: __t.string(), diff --git a/templates/quickstart-chat-typescript/src/module_bindings/set_name_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/set_name_type.ts index 6e321df4c9e..d3fe09ef92e 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/set_name_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/set_name_type.ts @@ -8,10 +8,8 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; -export default __t.object("SetName", { +export default __t.object('SetName', { name: __t.string(), }); - - diff --git a/templates/quickstart-chat-typescript/src/module_bindings/user_table.ts b/templates/quickstart-chat-typescript/src/module_bindings/user_table.ts index 4072746a6ac..93e32698c03 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/user_table.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/user_table.ts @@ -8,7 +8,7 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; export default __t.row({ identity: __t.identity().primaryKey(), diff --git a/templates/quickstart-chat-typescript/src/module_bindings/user_type.ts b/templates/quickstart-chat-typescript/src/module_bindings/user_type.ts index 15d5f14bfa4..89123cb9ad8 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/user_type.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/user_type.ts @@ -8,12 +8,10 @@ import { t as __t, type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, -} from "spacetimedb"; +} from 'spacetimedb'; -export default __t.object("User", { +export default __t.object('User', { identity: __t.identity(), name: __t.option(__t.string()), online: __t.bool(), }); - - From 07d86ec9da69daade5970391e8b8aca3cd7fd317 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Mon, 12 Jan 2026 09:34:00 -0800 Subject: [PATCH 07/11] Get it compiling with a querybuilder --- crates/bindings-typescript/src/sdk/client_api/index.ts | 4 +++- .../bindings-typescript/test-app/src/module_bindings/index.ts | 4 +++- crates/codegen/src/typescript.rs | 3 ++- .../codegen/tests/snapshots/codegen__codegen_typescript.snap | 3 ++- templates/basic-react/src/module_bindings/index.ts | 4 +++- templates/basic-typescript/src/module_bindings/index.ts | 4 +++- .../quickstart-chat-typescript/src/module_bindings/index.ts | 4 +++- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/bindings-typescript/src/sdk/client_api/index.ts b/crates/bindings-typescript/src/sdk/client_api/index.ts index 92ccd5a468e..a51ebc121d7 100644 --- a/crates/bindings-typescript/src/sdk/client_api/index.ts +++ b/crates/bindings-typescript/src/sdk/client_api/index.ts @@ -26,6 +26,7 @@ import { type Event as __Event, type EventContextInterface as __EventContextInterface, type Infer as __Infer, + type QueryBuilder as __QueryBuilder, type ReducerEventContextInterface as __ReducerEventContextInterface, type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, @@ -135,7 +136,8 @@ const REMOTE_MODULE = { export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); /** A typed query builder for this remote SpacetimeDB module. */ -export const query = __makeQueryBuilder(tablesSchema.schemaType); +export const query: __QueryBuilder = + __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( diff --git a/crates/bindings-typescript/test-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-app/src/module_bindings/index.ts index b12362ccf8a..db92e424599 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -26,6 +26,7 @@ import { type Event as __Event, type EventContextInterface as __EventContextInterface, type Infer as __Infer, + type QueryBuilder as __QueryBuilder, type ReducerEventContextInterface as __ReducerEventContextInterface, type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, @@ -126,7 +127,8 @@ const REMOTE_MODULE = { export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); /** A typed query builder for this remote SpacetimeDB module. */ -export const query = __makeQueryBuilder(tablesSchema.schemaType); +export const query: __QueryBuilder = + __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 0a25c2646dd..144e69e5a4a 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -351,7 +351,7 @@ impl Lang for TypeScript { writeln!(out, "/** A typed query builder for this remote SpacetimeDB module. */"); writeln!( out, - "export const query = __makeQueryBuilder(tablesSchema.schemaType);" + "export const query: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType);" ); writeln!(out); writeln!(out, "/** The reducers available in this remote SpacetimeDB module. */"); @@ -464,6 +464,7 @@ fn print_index_imports(out: &mut Indenter) { "DbConnectionBuilder as __DbConnectionBuilder", "convertToAccessorMap as __convertToAccessorMap", "makeQueryBuilder as __makeQueryBuilder", + "type QueryBuilder as __QueryBuilder", "type EventContextInterface as __EventContextInterface", "type ReducerEventContextInterface as __ReducerEventContextInterface", "type SubscriptionEventContextInterface as __SubscriptionEventContextInterface", diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index 7fb75d915c2..77550798baa 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -234,6 +234,7 @@ import { Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, makeQueryBuilder as __makeQueryBuilder, + type QueryBuilder as __QueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -561,7 +562,7 @@ const REMOTE_MODULE = { export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); /** A typed query builder for this remote SpacetimeDB module. */ -export const query = __makeQueryBuilder(tablesSchema.schemaType); +export const query: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); diff --git a/templates/basic-react/src/module_bindings/index.ts b/templates/basic-react/src/module_bindings/index.ts index 286aa9c45ba..d3559f43f5b 100644 --- a/templates/basic-react/src/module_bindings/index.ts +++ b/templates/basic-react/src/module_bindings/index.ts @@ -26,6 +26,7 @@ import { type Event as __Event, type EventContextInterface as __EventContextInterface, type Infer as __Infer, + type QueryBuilder as __QueryBuilder, type ReducerEventContextInterface as __ReducerEventContextInterface, type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, @@ -101,7 +102,8 @@ const REMOTE_MODULE = { export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); /** A typed query builder for this remote SpacetimeDB module. */ -export const query = __makeQueryBuilder(tablesSchema.schemaType); +export const query: __QueryBuilder = + __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( diff --git a/templates/basic-typescript/src/module_bindings/index.ts b/templates/basic-typescript/src/module_bindings/index.ts index 286aa9c45ba..d3559f43f5b 100644 --- a/templates/basic-typescript/src/module_bindings/index.ts +++ b/templates/basic-typescript/src/module_bindings/index.ts @@ -26,6 +26,7 @@ import { type Event as __Event, type EventContextInterface as __EventContextInterface, type Infer as __Infer, + type QueryBuilder as __QueryBuilder, type ReducerEventContextInterface as __ReducerEventContextInterface, type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, @@ -101,7 +102,8 @@ const REMOTE_MODULE = { export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); /** A typed query builder for this remote SpacetimeDB module. */ -export const query = __makeQueryBuilder(tablesSchema.schemaType); +export const query: __QueryBuilder = + __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( diff --git a/templates/quickstart-chat-typescript/src/module_bindings/index.ts b/templates/quickstart-chat-typescript/src/module_bindings/index.ts index aa58e322133..aea10f7daa6 100644 --- a/templates/quickstart-chat-typescript/src/module_bindings/index.ts +++ b/templates/quickstart-chat-typescript/src/module_bindings/index.ts @@ -26,6 +26,7 @@ import { type Event as __Event, type EventContextInterface as __EventContextInterface, type Infer as __Infer, + type QueryBuilder as __QueryBuilder, type ReducerEventContextInterface as __ReducerEventContextInterface, type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, @@ -121,7 +122,8 @@ const REMOTE_MODULE = { export const tables = __convertToAccessorMap(tablesSchema.schemaType.tables); /** A typed query builder for this remote SpacetimeDB module. */ -export const query = __makeQueryBuilder(tablesSchema.schemaType); +export const query: __QueryBuilder = + __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( From 4782ff02886e6328b255a3a82a764946b0961e54 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Mon, 12 Jan 2026 10:51:09 -0800 Subject: [PATCH 08/11] Get the test app working with the query builder --- .../src/sdk/subscription_builder_impl.ts | 16 ++++++++++++++-- crates/bindings-typescript/test-app/src/main.tsx | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/bindings-typescript/src/sdk/subscription_builder_impl.ts b/crates/bindings-typescript/src/sdk/subscription_builder_impl.ts index 851b6baffe0..ae3f32071cc 100644 --- a/crates/bindings-typescript/src/sdk/subscription_builder_impl.ts +++ b/crates/bindings-typescript/src/sdk/subscription_builder_impl.ts @@ -5,6 +5,7 @@ import type { } from './event_context'; import { EventEmitter } from './event_emitter'; import type { UntypedRemoteModule } from './spacetime_module'; +import { isRowTypedQuery, toSql, type RowTypedQuery } from '../lib/query'; export class SubscriptionBuilderImpl { #onApplied?: (ctx: SubscriptionEventContextInterface) => void = @@ -78,15 +79,26 @@ export class SubscriptionBuilderImpl { * ``` */ subscribe( - query_sql: string | string[] + query_sql: string | RowTypedQuery + ): SubscriptionHandleImpl; + subscribe( + query_sql: Array> + ): SubscriptionHandleImpl; + subscribe( + query_sql: string | RowTypedQuery | Array> ): SubscriptionHandleImpl { const queries = Array.isArray(query_sql) ? query_sql : [query_sql]; if (queries.length === 0) { throw new Error('Subscriptions must have at least one query'); } + const queryStrings = queries.map(q => { + if (typeof q === 'string') return q; + if (isRowTypedQuery(q)) return toSql(q); + throw new Error('Subscriptions must be SQL strings or typed queries'); + }); return new SubscriptionHandleImpl( this.db, - queries, + queryStrings, this.#onApplied, this.#onError ); diff --git a/crates/bindings-typescript/test-app/src/main.tsx b/crates/bindings-typescript/test-app/src/main.tsx index 028148b3eb7..fb1ac03c242 100644 --- a/crates/bindings-typescript/test-app/src/main.tsx +++ b/crates/bindings-typescript/test-app/src/main.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; import { SpacetimeDBProvider } from '../../src/react'; -import { DbConnection } from './module_bindings/index.ts'; +import { DbConnection, query } from './module_bindings/index.ts'; const connectionBuilder = DbConnection.builder() .withUri('ws://localhost:3000') @@ -21,7 +21,7 @@ const connectionBuilder = DbConnection.builder() identity.toHexString() ); - conn.subscriptionBuilder().subscribe('SELECT * FROM player'); + conn.subscriptionBuilder().subscribe(query.player.build()); }); ReactDOM.createRoot(document.getElementById('root')!).render( From d67d2bb83042ed3cf9039ee2b0f1c8341a48c1ce Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Mon, 12 Jan 2026 13:22:10 -0800 Subject: [PATCH 09/11] Add bindings for some templates --- .../basic-react/src/module_bindings/add_type.ts | 15 +++++++++++++++ .../basic-react/src/module_bindings/init_type.ts | 13 +++++++++++++ .../src/module_bindings/on_connect_type.ts | 13 +++++++++++++ .../src/module_bindings/on_disconnect_type.ts | 13 +++++++++++++ .../src/module_bindings/say_hello_type.ts | 13 +++++++++++++ .../src/module_bindings/add_type.ts | 15 +++++++++++++++ .../src/module_bindings/init_type.ts | 13 +++++++++++++ .../src/module_bindings/on_connect_type.ts | 13 +++++++++++++ .../src/module_bindings/on_disconnect_type.ts | 13 +++++++++++++ .../src/module_bindings/say_hello_type.ts | 13 +++++++++++++ 10 files changed, 134 insertions(+) create mode 100644 templates/basic-react/src/module_bindings/add_type.ts create mode 100644 templates/basic-react/src/module_bindings/init_type.ts create mode 100644 templates/basic-react/src/module_bindings/on_connect_type.ts create mode 100644 templates/basic-react/src/module_bindings/on_disconnect_type.ts create mode 100644 templates/basic-react/src/module_bindings/say_hello_type.ts create mode 100644 templates/basic-typescript/src/module_bindings/add_type.ts create mode 100644 templates/basic-typescript/src/module_bindings/init_type.ts create mode 100644 templates/basic-typescript/src/module_bindings/on_connect_type.ts create mode 100644 templates/basic-typescript/src/module_bindings/on_disconnect_type.ts create mode 100644 templates/basic-typescript/src/module_bindings/say_hello_type.ts diff --git a/templates/basic-react/src/module_bindings/add_type.ts b/templates/basic-react/src/module_bindings/add_type.ts new file mode 100644 index 00000000000..638f62cea39 --- /dev/null +++ b/templates/basic-react/src/module_bindings/add_type.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('Add', { + name: __t.string(), +}); diff --git a/templates/basic-react/src/module_bindings/init_type.ts b/templates/basic-react/src/module_bindings/init_type.ts new file mode 100644 index 00000000000..52ed691ed94 --- /dev/null +++ b/templates/basic-react/src/module_bindings/init_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('Init', {}); diff --git a/templates/basic-react/src/module_bindings/on_connect_type.ts b/templates/basic-react/src/module_bindings/on_connect_type.ts new file mode 100644 index 00000000000..d36362515de --- /dev/null +++ b/templates/basic-react/src/module_bindings/on_connect_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('OnConnect', {}); diff --git a/templates/basic-react/src/module_bindings/on_disconnect_type.ts b/templates/basic-react/src/module_bindings/on_disconnect_type.ts new file mode 100644 index 00000000000..efda71ebcfd --- /dev/null +++ b/templates/basic-react/src/module_bindings/on_disconnect_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('OnDisconnect', {}); diff --git a/templates/basic-react/src/module_bindings/say_hello_type.ts b/templates/basic-react/src/module_bindings/say_hello_type.ts new file mode 100644 index 00000000000..6293ca6bd09 --- /dev/null +++ b/templates/basic-react/src/module_bindings/say_hello_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('SayHello', {}); diff --git a/templates/basic-typescript/src/module_bindings/add_type.ts b/templates/basic-typescript/src/module_bindings/add_type.ts new file mode 100644 index 00000000000..638f62cea39 --- /dev/null +++ b/templates/basic-typescript/src/module_bindings/add_type.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('Add', { + name: __t.string(), +}); diff --git a/templates/basic-typescript/src/module_bindings/init_type.ts b/templates/basic-typescript/src/module_bindings/init_type.ts new file mode 100644 index 00000000000..52ed691ed94 --- /dev/null +++ b/templates/basic-typescript/src/module_bindings/init_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('Init', {}); diff --git a/templates/basic-typescript/src/module_bindings/on_connect_type.ts b/templates/basic-typescript/src/module_bindings/on_connect_type.ts new file mode 100644 index 00000000000..d36362515de --- /dev/null +++ b/templates/basic-typescript/src/module_bindings/on_connect_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('OnConnect', {}); diff --git a/templates/basic-typescript/src/module_bindings/on_disconnect_type.ts b/templates/basic-typescript/src/module_bindings/on_disconnect_type.ts new file mode 100644 index 00000000000..efda71ebcfd --- /dev/null +++ b/templates/basic-typescript/src/module_bindings/on_disconnect_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('OnDisconnect', {}); diff --git a/templates/basic-typescript/src/module_bindings/say_hello_type.ts b/templates/basic-typescript/src/module_bindings/say_hello_type.ts new file mode 100644 index 00000000000..6293ca6bd09 --- /dev/null +++ b/templates/basic-typescript/src/module_bindings/say_hello_type.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from 'spacetimedb'; + +export default __t.object('SayHello', {}); From 007e6f16a6b1b698d6a8df1f08464dda335184db Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Tue, 13 Jan 2026 10:31:18 -0800 Subject: [PATCH 10/11] Update snap --- crates/codegen/tests/snapshots/codegen__codegen_typescript.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index 77550798baa..c75ae4e12ff 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -234,7 +234,6 @@ import { Uuid as __Uuid, convertToAccessorMap as __convertToAccessorMap, makeQueryBuilder as __makeQueryBuilder, - type QueryBuilder as __QueryBuilder, procedureSchema as __procedureSchema, procedures as __procedures, reducerSchema as __reducerSchema, @@ -248,6 +247,7 @@ import { type Event as __Event, type EventContextInterface as __EventContextInterface, type Infer as __Infer, + type QueryBuilder as __QueryBuilder, type ReducerEventContextInterface as __ReducerEventContextInterface, type RemoteModule as __RemoteModule, type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, From 257ee8221093ab6da8df598482384ed519ef5e67 Mon Sep 17 00:00:00 2001 From: Jeffrey Dallatezza Date: Thu, 15 Jan 2026 09:00:11 -0800 Subject: [PATCH 11/11] Add query builder tests using test-app module bindings --- .../tests/client_query.test.ts | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 crates/bindings-typescript/tests/client_query.test.ts diff --git a/crates/bindings-typescript/tests/client_query.test.ts b/crates/bindings-typescript/tests/client_query.test.ts new file mode 100644 index 00000000000..9c8350ac742 --- /dev/null +++ b/crates/bindings-typescript/tests/client_query.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, it } from 'vitest'; +import { Identity } from '../src/lib/identity'; +import { and, not, or, toSql } from '../src/lib/query'; +import { query } from '../test-app/src/module_bindings'; + +describe('ClientQuery.toSql', () => { + it('renders a full-table scan when no filters are applied', () => { + const sql = toSql(query.player.build()); + + expect(sql).toBe('SELECT * FROM "player"'); + }); + + it('renders a WHERE clause for simple equality filters', () => { + const sql = toSql( + query.player + .where(row => row.name.eq("O'Brian")) + .build() + ); + + expect(sql).toBe( + `SELECT * FROM "player" WHERE "player"."name" = 'O''Brian'` + ); + }); + + it('renders numeric literals and column references', () => { + const sql = toSql( + query.player + .where(row => row.id.eq(42)) + .build() + ); + + expect(sql).toBe(`SELECT * FROM "player" WHERE "player"."id" = 42`); + }); + + it('renders AND clauses across multiple predicates', () => { + const sql = toSql( + query.player + .where(row => and(row.name.eq('Alice'), row.id.eq(30))) + .build() + ); + + expect(sql).toBe( + `SELECT * FROM "player" WHERE ("player"."name" = 'Alice') AND ("player"."id" = 30)` + ); + }); + + it('renders NOT clauses around subpredicates', () => { + const sql = toSql( + query.player + .where(row => not(row.name.eq('Bob'))) + .build() + ); + + expect(sql).toBe( + `SELECT * FROM "player" WHERE NOT ("player"."name" = 'Bob')` + ); + }); + + it('accumulates multiple filters with AND logic', () => { + const sql = toSql( + query.player + .where(row => row.name.eq('Eve')) + .where(row => row.id.eq(25)) + .build() + ); + + expect(sql).toBe( + `SELECT * FROM "player" WHERE ("player"."name" = 'Eve') AND ("player"."id" = 25)` + ); + }); + + it('renders OR clauses across multiple predicates', () => { + const sql = toSql( + query.player + .where(row => or(row.name.eq('Carol'), row.name.eq('Dave'))) + .build() + ); + + expect(sql).toBe( + `SELECT * FROM "player" WHERE ("player"."name" = 'Carol') OR ("player"."name" = 'Dave')` + ); + }); + + it('renders Identity literals using their hex form', () => { + const identity = new Identity( + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + ); + const sql = toSql( + query.user + .where(row => row.identity.eq(identity)) + .build() + ); + + expect(sql).toBe( + `SELECT * FROM "user" WHERE "user"."identity" = 0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef` + ); + }); + + it('renders semijoin queries without additional filters', () => { + const sql = toSql( + query.player + .rightSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .build() + ); + + expect(sql).toBe( + `SELECT "unindexed_player".* FROM "player" JOIN "unindexed_player" ON "unindexed_player"."id" = "player"."id"` + ); + }); + + it('renders semijoin queries alongside existing predicates', () => { + const sql = toSql( + query.player + .where(row => row.id.eq(42)) + .rightSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .build() + ); + + expect(sql).toBe( + `SELECT "unindexed_player".* FROM "player" JOIN "unindexed_player" ON "unindexed_player"."id" = "player"."id" WHERE "player"."id" = 42` + ); + }); + + it('escapes literals when rendering semijoin filters', () => { + const sql = toSql( + query.player + .where(row => row.name.eq("O'Brian")) + .rightSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .build() + ); + + expect(sql).toBe( + `SELECT "unindexed_player".* FROM "player" JOIN "unindexed_player" ON "unindexed_player"."id" = "player"."id" WHERE "player"."name" = 'O''Brian'` + ); + }); + + it('renders compound AND filters for semijoin queries', () => { + const sql = toSql( + query.player + .where(row => and(row.name.eq('Alice'), row.id.eq(30))) + .rightSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .build() + ); + + expect(sql).toBe( + `SELECT "unindexed_player".* FROM "player" JOIN "unindexed_player" ON "unindexed_player"."id" = "player"."id" WHERE ("player"."name" = 'Alice') AND ("player"."id" = 30)` + ); + }); + + it('basic where', () => { + const sql = toSql(query.player.where(row => row.name.eq('Gadget')).build()); + expect(sql).toBe( + `SELECT * FROM "player" WHERE "player"."name" = 'Gadget'` + ); + }); + + it('basic where lt', () => { + const sql = toSql(query.player.where(row => row.name.lt('Gadget')).build()); + expect(sql).toBe( + `SELECT * FROM "player" WHERE "player"."name" < 'Gadget'` + ); + }); + + it('basic where lte', () => { + const sql = toSql(query.player.where(row => row.name.lte('Gadget')).build()); + expect(sql).toBe( + `SELECT * FROM "player" WHERE "player"."name" <= 'Gadget'` + ); + }); + + it('basic where gt', () => { + const sql = toSql(query.player.where(row => row.name.gt('Gadget')).build()); + expect(sql).toBe( + `SELECT * FROM "player" WHERE "player"."name" > 'Gadget'` + ); + }); + + it('basic where gte', () => { + const sql = toSql(query.player.where(row => row.name.gte('Gadget')).build()); + expect(sql).toBe( + `SELECT * FROM "player" WHERE "player"."name" >= 'Gadget'` + ); + }); + + it('basic semijoin', () => { + const sql = toSql( + query.player + .rightSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .build() + ); + expect(sql).toBe( + `SELECT "unindexed_player".* FROM "player" JOIN "unindexed_player" ON "unindexed_player"."id" = "player"."id"` + ); + }); + + it('basic left semijoin', () => { + const sql = toSql( + query.player + .leftSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .build() + ); + expect(sql).toBe( + `SELECT "player".* FROM "unindexed_player" JOIN "player" ON "unindexed_player"."id" = "player"."id"` + ); + }); + + it('semijoin with filters on both sides', () => { + const sql = toSql( + query.player + .where(row => row.id.eq(42)) + .rightSemijoin(query.unindexed_player, (player, other) => + other.id.eq(player.id) + ) + .where(row => row.name.eq('Gadget')) + .build() + ); + expect(sql).toBe( + `SELECT "unindexed_player".* FROM "player" JOIN "unindexed_player" ON "unindexed_player"."id" = "player"."id" WHERE ("player"."id" = 42) AND ("unindexed_player"."name" = 'Gadget')` + ); + }); +});