From bc82c6efc8d8c8946f2af1411f104341246b092f Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 19:12:19 -0400 Subject: [PATCH 1/4] Fix TypeScript table handle casing --- .../src/module_bindings/index.ts | 2 +- .../test-app/src/module_bindings/index.ts | 4 ++-- .../src/module_bindings/index.ts | 2 +- .../src/module_bindings/index.ts | 2 +- .../tests/client_query.test.ts | 22 +++++++++---------- .../tests/db_connection.test.ts | 10 ++++----- .../tests/query_error_message.test.ts | 2 +- .../tests/table_cache.test.ts | 4 ++-- crates/codegen/src/typescript.rs | 6 +++-- crates/codegen/tests/codegen.rs | 15 +++++++++++++ .../codegen__codegen_typescript.snap | 8 +++---- .../client-ts/src/game/GameManager.ts | 2 +- .../client-ts/src/module_bindings/index.ts | 2 +- templates/hangman-react-ts/src/App.tsx | 2 +- .../src/module_bindings/index.ts | 2 +- templates/money-exchange-react-ts/src/App.tsx | 4 ++-- .../src/module_bindings/index.ts | 4 ++-- 17 files changed, 55 insertions(+), 38 deletions(-) diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts index 63c1524da89..4693792f6de 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts @@ -97,7 +97,7 @@ const tablesSchema = __schema({ }, Person2Row ), - person_at_level_2: __table( + personAtLevel2: __table( { name: 'Level2Person', indexes: [], 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 49090beab53..6b58328915a 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -65,7 +65,7 @@ const tablesSchema = __schema({ }, PlayerRow ), - unindexed_player: __table( + unindexedPlayer: __table( { name: 'unindexed_player', indexes: [ @@ -107,7 +107,7 @@ const tablesSchema = __schema({ }, UserRow ), - my_user_procedural: __table( + myUserProcedural: __table( { name: 'my_user_procedural', indexes: [], diff --git a/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts index b9d553082d4..1d6ae15bf69 100644 --- a/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts @@ -65,7 +65,7 @@ const tablesSchema = __schema({ }, CounterRow ), - offline_user: __table( + offlineUser: __table( { name: 'offline_user', indexes: [ diff --git a/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts b/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts index b9d553082d4..1d6ae15bf69 100644 --- a/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts @@ -65,7 +65,7 @@ const tablesSchema = __schema({ }, CounterRow ), - offline_user: __table( + offlineUser: __table( { name: 'offline_user', indexes: [ diff --git a/crates/bindings-typescript/tests/client_query.test.ts b/crates/bindings-typescript/tests/client_query.test.ts index b29a44ba185..9da4eaee804 100644 --- a/crates/bindings-typescript/tests/client_query.test.ts +++ b/crates/bindings-typescript/tests/client_query.test.ts @@ -89,7 +89,7 @@ describe('ClientQuery.toSql', () => { it('renders semijoin queries without additional filters', () => { const sql = toSql( tables.player - .rightSemijoin(tables.unindexed_player, (player, other) => + .rightSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .build() @@ -104,7 +104,7 @@ describe('ClientQuery.toSql', () => { const sql = toSql( tables.player .where(row => row.id.eq(42)) - .rightSemijoin(tables.unindexed_player, (player, other) => + .rightSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .build() @@ -119,7 +119,7 @@ describe('ClientQuery.toSql', () => { const sql = toSql( tables.player .where(row => row.name.eq("O'Brian")) - .rightSemijoin(tables.unindexed_player, (player, other) => + .rightSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .build() @@ -134,7 +134,7 @@ describe('ClientQuery.toSql', () => { const sql = toSql( tables.player .where(row => and(row.name.eq('Alice'), row.id.eq(30))) - .rightSemijoin(tables.unindexed_player, (player, other) => + .rightSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .build() @@ -196,7 +196,7 @@ describe('ClientQuery.toSql', () => { it('basic semijoin', () => { const sql = toSql( tables.player - .rightSemijoin(tables.unindexed_player, (player, other) => + .rightSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .build() @@ -209,7 +209,7 @@ describe('ClientQuery.toSql', () => { it('basic left semijoin', () => { const sql = toSql( tables.player - .leftSemijoin(tables.unindexed_player, (player, other) => + .leftSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .build() @@ -252,7 +252,7 @@ describe('ClientQuery.toSql', () => { const sql = toSql( tables.player .where(row => row.id.eq(42)) - .rightSemijoin(tables.unindexed_player, (player, other) => + .rightSemijoin(tables.unindexedPlayer, (player, other) => other.id.eq(player.id) ) .where(row => row.name.eq('Gadget')) @@ -286,7 +286,7 @@ describe('useTable type compatibility', () => { it('rightSemijoin result is assignable to useTable query param', () => { assertType( - tables.player.rightSemijoin(tables.unindexed_player, (p, o) => + tables.player.rightSemijoin(tables.unindexedPlayer, (p, o) => o.id.eq(p.id) ) ); @@ -294,7 +294,7 @@ describe('useTable type compatibility', () => { it('leftSemijoin result is assignable to useTable query param', () => { assertType( - tables.player.leftSemijoin(tables.unindexed_player, (p, o) => + tables.player.leftSemijoin(tables.unindexedPlayer, (p, o) => o.id.eq(p.id) ) ); @@ -303,7 +303,7 @@ describe('useTable type compatibility', () => { it('semijoin with .where() is assignable to useTable query param', () => { assertType( tables.player - .rightSemijoin(tables.unindexed_player, (p, o) => o.id.eq(p.id)) + .rightSemijoin(tables.unindexedPlayer, (p, o) => o.id.eq(p.id)) .where(r => r.name.eq('test')) ); }); @@ -320,7 +320,7 @@ describe('useTable type compatibility', () => { it('semijoin result exposes toSql() returning string', () => { const sql: string = tables.player - .rightSemijoin(tables.unindexed_player, (p, o) => o.id.eq(p.id)) + .rightSemijoin(tables.unindexedPlayer, (p, o) => o.id.eq(p.id)) .toSql(); expect(typeof sql).toBe('string'); }); diff --git a/crates/bindings-typescript/tests/db_connection.test.ts b/crates/bindings-typescript/tests/db_connection.test.ts index 7a0da07bf52..b2b29885f9d 100644 --- a/crates/bindings-typescript/tests/db_connection.test.ts +++ b/crates/bindings-typescript/tests/db_connection.test.ts @@ -847,11 +847,11 @@ describe('DbConnection', () => { // `onUpdate` is only available when the generated view row binding carries // primary-key metadata. - client.db.my_user_procedural.onInsert((_ctx, row) => { + client.db.myUserProcedural.onInsert((_ctx, row) => { expect(row).toEqual(initialRow); initialInsertPromise.resolve(); }); - client.db.my_user_procedural.onUpdate((_ctx, oldRow, newRow) => { + client.db.myUserProcedural.onUpdate((_ctx, oldRow, newRow) => { updates.push({ oldRow, newRow, @@ -876,7 +876,7 @@ describe('DbConnection', () => { await initialInsertPromise.promise; expect(client.db.player.count()).toEqual(1n); - expect(client.db.my_user_procedural.count()).toEqual(1n); + expect(client.db.myUserProcedural.count()).toEqual(1n); // A delete and insert with the same primary key in one transaction should // be coalesced by the client cache into `onUpdate`, not separate delete and @@ -909,8 +909,8 @@ describe('DbConnection', () => { }, ]); expect(client.db.player.count()).toEqual(1n); - expect(client.db.my_user_procedural.count()).toEqual(1n); - expect([...client.db.my_user_procedural.iter()][0]).toEqual(updatedRow); + expect(client.db.myUserProcedural.count()).toEqual(1n); + expect([...client.db.myUserProcedural.iter()][0]).toEqual(updatedRow); }); test('Filtering works', async () => { diff --git a/crates/bindings-typescript/tests/query_error_message.test.ts b/crates/bindings-typescript/tests/query_error_message.test.ts index f016c97cc28..20a9285258b 100644 --- a/crates/bindings-typescript/tests/query_error_message.test.ts +++ b/crates/bindings-typescript/tests/query_error_message.test.ts @@ -28,7 +28,7 @@ import { and } from ${JSON.stringify(imports.query)}; import { tables } from ${JSON.stringify(imports.moduleBindings)}; tables.player - .leftSemijoin(tables.unindexed_player, (l, r) => ${semijoinPredicateExpr}) + .leftSemijoin(tables.unindexedPlayer, (l, r) => ${semijoinPredicateExpr}) .build(); `; diff --git a/crates/bindings-typescript/tests/table_cache.test.ts b/crates/bindings-typescript/tests/table_cache.test.ts index a3b1f2becc6..558faf06b48 100644 --- a/crates/bindings-typescript/tests/table_cache.test.ts +++ b/crates/bindings-typescript/tests/table_cache.test.ts @@ -101,13 +101,13 @@ function runTest( describe('TableCache', () => { describe('Unindexed player table', () => { - const newTable = () => new TableCacheImpl(tables.unindexed_player.tableDef); + const newTable = () => new TableCacheImpl(tables.unindexedPlayer.tableDef); const mkOperation = ( type: 'insert' | 'delete', row: Infer ) => { const rowId = AlgebraicType.intoMapKey( - { tag: 'Product', value: tables.unindexed_player.rowType }, + { tag: 'Product', value: tables.unindexedPlayer.rowType }, row ); return { diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 28bc1fb4c91..1f326044fb0 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -198,8 +198,9 @@ impl Lang for TypeScript { out.indent(1); for table in iter_tables(module, options.visibility) { let type_ref = table.product_type_ref; + let table_accessor = table.accessor_name.deref().to_case(Case::Camel); let table_name_pascalcase = table.accessor_name.deref().to_case(Case::Pascal); - writeln!(out, "{}: __table({{", table.accessor_name); + writeln!(out, "{table_accessor}: __table({{"); out.indent(1); write_table_opts( module, @@ -215,8 +216,9 @@ impl Lang for TypeScript { } for view in iter_views(module) { let type_ref = view.product_type_ref; + let view_accessor = view.accessor_name.deref().to_case(Case::Camel); let view_name_pascalcase = view.accessor_name.deref().to_case(Case::Pascal); - writeln!(out, "{}: __table({{", view.accessor_name); + writeln!(out, "{view_accessor}: __table({{"); out.indent(1); write_table_opts(module, out, type_ref, &view.name, iter::empty(), iter::empty(), false); out.dedent(1); diff --git a/crates/codegen/tests/codegen.rs b/crates/codegen/tests/codegen.rs index 06dc3ebe8fc..7eda97c0a99 100644 --- a/crates/codegen/tests/codegen.rs +++ b/crates/codegen/tests/codegen.rs @@ -39,3 +39,18 @@ declare_tests! { test_codegen_typescript => TypeScript, test_codegen_rust => Rust, } + +#[test] +fn test_typescript_table_handles_are_camel_case() { + let module = compiled_module(); + let index = generate(module, &TypeScript, &CodegenOptions::default()) + .into_iter() + .find(|file| file.filename == "index.ts") + .expect("typescript codegen should emit index.ts") + .code; + + assert!(index.contains("loggedOutPlayer: __table({")); + assert!(!index.contains("logged_out_player: __table({")); + assert!(index.contains("myPlayer: __table({")); + assert!(!index.contains("my_player: __table({")); +} diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index 8616888c54f..aa977c6a8ac 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -187,7 +187,7 @@ import TestFRow from "./test_f_table"; /** 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({ - logged_out_player: __table({ + loggedOutPlayer: __table({ name: 'logged_out_player', indexes: [ { accessor: 'identity', name: 'logged_out_player_identity_idx_btree', algorithm: 'btree', columns: [ @@ -239,21 +239,21 @@ const tablesSchema = __schema({ { name: 'player_player_id_key', constraint: 'unique', columns: ['playerId'] }, ], }, PlayerRow), - test_d: __table({ + testD: __table({ name: 'test_d', indexes: [ ], constraints: [ ], }, TestDRow), - test_f: __table({ + testF: __table({ name: 'test_f', indexes: [ ], constraints: [ ], }, TestFRow), - my_player: __table({ + myPlayer: __table({ name: 'my_player', indexes: [ ], diff --git a/demo/Blackholio/client-ts/src/game/GameManager.ts b/demo/Blackholio/client-ts/src/game/GameManager.ts index e350c108083..1265434a17b 100644 --- a/demo/Blackholio/client-ts/src/game/GameManager.ts +++ b/demo/Blackholio/client-ts/src/game/GameManager.ts @@ -104,7 +104,7 @@ export class GameManager { connection.db.player.onDelete((_ctx, player) => { this.players.delete(player.playerId); }); - connection.db.consume_entity_event.onInsert((_ctx, event) => + connection.db.consumeEntityEvent.onInsert((_ctx, event) => this.consumeEntityEventOnInsert(event) ); } diff --git a/demo/Blackholio/client-ts/src/module_bindings/index.ts b/demo/Blackholio/client-ts/src/module_bindings/index.ts index 6120bf25542..a05ac44a41b 100644 --- a/demo/Blackholio/client-ts/src/module_bindings/index.ts +++ b/demo/Blackholio/client-ts/src/module_bindings/index.ts @@ -79,7 +79,7 @@ const tablesSchema = __schema({ { name: 'config_id_key', constraint: 'unique', columns: ['id'] }, ], }, ConfigRow), - consume_entity_event: __table({ + consumeEntityEvent: __table({ name: 'consume_entity_event', indexes: [ ], diff --git a/templates/hangman-react-ts/src/App.tsx b/templates/hangman-react-ts/src/App.tsx index 6f80c29884e..64486fb9755 100644 --- a/templates/hangman-react-ts/src/App.tsx +++ b/templates/hangman-react-ts/src/App.tsx @@ -192,7 +192,7 @@ function App() { const { identity, isActive: connected } = useSpacetimeDB(); const [rounds] = useTable(tables.currentRound); const [players] = useTable(tables.player); - const [boards] = useTable(tables.my_progress); + const [boards] = useTable(tables.myProgress); const [results] = useTable(tables.roundResult); const setName = useReducer(reducers.setName); const guessLetter = useReducer(reducers.guessLetter); diff --git a/templates/hangman-react-ts/src/module_bindings/index.ts b/templates/hangman-react-ts/src/module_bindings/index.ts index 8ca848b15a5..1c459675772 100644 --- a/templates/hangman-react-ts/src/module_bindings/index.ts +++ b/templates/hangman-react-ts/src/module_bindings/index.ts @@ -108,7 +108,7 @@ const tablesSchema = __schema({ }, RoundResultRow ), - my_progress: __table( + myProgress: __table( { name: 'my_progress', indexes: [], diff --git a/templates/money-exchange-react-ts/src/App.tsx b/templates/money-exchange-react-ts/src/App.tsx index c45ee166e44..85c838bb66c 100644 --- a/templates/money-exchange-react-ts/src/App.tsx +++ b/templates/money-exchange-react-ts/src/App.tsx @@ -118,9 +118,9 @@ function NameForm({ function App() { const { identity, isActive: connected } = useSpacetimeDB(); - const [accounts] = useTable(tables.my_account); + const [accounts] = useTable(tables.myAccount); const [directory] = useTable(tables.directory); - const [changes] = useTable(tables.my_account_changes); + const [changes] = useTable(tables.myAccountChanges); const setName = useReducer(reducers.setName); const sendTransfer = useReducer(reducers.transfer); const [editingName, setEditingName] = useState(false); diff --git a/templates/money-exchange-react-ts/src/module_bindings/index.ts b/templates/money-exchange-react-ts/src/module_bindings/index.ts index b41cebe950a..e5b102c7132 100644 --- a/templates/money-exchange-react-ts/src/module_bindings/index.ts +++ b/templates/money-exchange-react-ts/src/module_bindings/index.ts @@ -80,7 +80,7 @@ const tablesSchema = __schema({ }, DirectoryRow ), - my_account: __table( + myAccount: __table( { name: 'my_account', indexes: [], @@ -88,7 +88,7 @@ const tablesSchema = __schema({ }, MyAccountRow ), - my_account_changes: __table( + myAccountChanges: __table( { name: 'my_account_changes', indexes: [], From 34c0e1b99e2a6c1de28d06ebbce139cff119c5ea Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sat, 13 Jun 2026 17:21:10 -0400 Subject: [PATCH 2/4] Keep deprecated TypeScript table handle aliases --- .../src/module_bindings/index.ts | 114 +++++++- .../test-app/src/module_bindings/index.ts | 104 +++++++- .../src/module_bindings/index.ts | 94 ++++++- .../src/module_bindings/index.ts | 94 ++++++- .../tests/client_query.test.ts | 9 + .../tests/db_connection.test.ts | 15 +- crates/codegen/src/typescript.rs | 252 ++++++++++++++++-- crates/codegen/tests/codegen.rs | 6 + .../codegen__codegen_typescript.snap | 82 +++++- .../client-ts/src/module_bindings/index.ts | 62 ++++- .../src/module_bindings/index.ts | 114 +++++++- .../src/module_bindings/index.ts | 104 +++++++- 12 files changed, 940 insertions(+), 110 deletions(-) diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts index 4693792f6de..efec18b01e5 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts @@ -117,23 +117,98 @@ const reducersSchema = __reducers( /** 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(); +type __SchemaWithTableAccessorAliases = Omit< + typeof tablesSchema.schemaType, + 'tables' +> & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `player1` instead. This alias will be removed in the next major version. */ + readonly player_1: Omit< + (typeof tablesSchema.schemaType.tables)['player1'], + 'accessorName' + > & { readonly accessorName: 'player_1' }; + /** @deprecated Use `person2` instead. This alias will be removed in the next major version. */ + readonly person_2: Omit< + (typeof tablesSchema.schemaType.tables)['person2'], + 'accessorName' + > & { readonly accessorName: 'person_2' }; + /** @deprecated Use `personAtLevel2` instead. This alias will be removed in the next major version. */ + readonly person_at_level_2: Omit< + (typeof tablesSchema.schemaType.tables)['personAtLevel2'], + 'accessorName' + > & { readonly accessorName: 'person_at_level_2' }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: '2.5.0' as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType + .tables as __SchemaWithTableAccessorAliases['tables'], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + player_1: 'player1', + person_2: 'person2', + person_at_level_2: 'personAtLevel2', +} as const; + +function __withTableAccessorAliases( + target: T, + freeze = false +): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & + Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + tableAccessorAliases + )) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl['db']; +export type DbView = __DbViewBase & { + /** @deprecated Use `player1` instead. This alias will be removed in the next major version. */ + readonly player_1: __DbViewBase['player1']; + /** @deprecated Use `person2` instead. This alias will be removed in the next major version. */ + readonly person_2: __DbViewBase['person2']; + /** @deprecated Use `personAtLevel2` instead. This alias will be removed in the next major version. */ + readonly person_at_level_2: __DbViewBase['personAtLevel2']; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `player1` instead. This alias will be removed in the next major version. */ + readonly player_1: __TablesBase['player1']; + /** @deprecated Use `person2` instead. This alias will be removed in the next major version. */ + readonly person_2: __TablesBase['person2']; + /** @deprecated Use `personAtLevel2` instead. This alias will be removed in the next major version. */ + readonly person_at_level_2: __TablesBase['personAtLevel2']; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = - __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases( + tablesBase, + true +) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( @@ -144,17 +219,25 @@ export const reducers = __convertToAccessorMap( export const procedures = __convertToAccessorMap(proceduresSchema.procedures); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit< + __EventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface< - typeof REMOTE_MODULE ->; +export type ReducerEventContext = Omit< + __ReducerEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface< - typeof REMOTE_MODULE ->; +export type SubscriptionEventContext = Omit< + __SubscriptionEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit< + __ErrorContextInterface, + 'db' +> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -168,6 +251,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { return new DbConnectionBuilder( 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 6b58328915a..bd9d5caf88f 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -125,23 +125,88 @@ const reducersSchema = __reducers( /** 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(); +type __SchemaWithTableAccessorAliases = Omit< + typeof tablesSchema.schemaType, + 'tables' +> & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `unindexedPlayer` instead. This alias will be removed in the next major version. */ + readonly unindexed_player: Omit< + (typeof tablesSchema.schemaType.tables)['unindexedPlayer'], + 'accessorName' + > & { readonly accessorName: 'unindexed_player' }; + /** @deprecated Use `myUserProcedural` instead. This alias will be removed in the next major version. */ + readonly my_user_procedural: Omit< + (typeof tablesSchema.schemaType.tables)['myUserProcedural'], + 'accessorName' + > & { readonly accessorName: 'my_user_procedural' }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: '2.3.0' as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType + .tables as __SchemaWithTableAccessorAliases['tables'], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + unindexed_player: 'unindexedPlayer', + my_user_procedural: 'myUserProcedural', +} as const; + +function __withTableAccessorAliases( + target: T, + freeze = false +): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & + Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + tableAccessorAliases + )) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl['db']; +export type DbView = __DbViewBase & { + /** @deprecated Use `unindexedPlayer` instead. This alias will be removed in the next major version. */ + readonly unindexed_player: __DbViewBase['unindexedPlayer']; + /** @deprecated Use `myUserProcedural` instead. This alias will be removed in the next major version. */ + readonly my_user_procedural: __DbViewBase['myUserProcedural']; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `unindexedPlayer` instead. This alias will be removed in the next major version. */ + readonly unindexed_player: __TablesBase['unindexedPlayer']; + /** @deprecated Use `myUserProcedural` instead. This alias will be removed in the next major version. */ + readonly my_user_procedural: __TablesBase['myUserProcedural']; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = - __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases( + tablesBase, + true +) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( @@ -152,17 +217,25 @@ export const reducers = __convertToAccessorMap( export const procedures = __convertToAccessorMap(proceduresSchema.procedures); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit< + __EventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface< - typeof REMOTE_MODULE ->; +export type ReducerEventContext = Omit< + __ReducerEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface< - typeof REMOTE_MODULE ->; +export type SubscriptionEventContext = Omit< + __SubscriptionEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit< + __ErrorContextInterface, + 'db' +> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -176,6 +249,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { return new DbConnectionBuilder( diff --git a/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts index 1d6ae15bf69..a0beb693388 100644 --- a/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts @@ -118,23 +118,78 @@ const reducersSchema = __reducers( /** 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(); +type __SchemaWithTableAccessorAliases = Omit< + typeof tablesSchema.schemaType, + 'tables' +> & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `offlineUser` instead. This alias will be removed in the next major version. */ + readonly offline_user: Omit< + (typeof tablesSchema.schemaType.tables)['offlineUser'], + 'accessorName' + > & { readonly accessorName: 'offline_user' }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: '2.0.0' as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType + .tables as __SchemaWithTableAccessorAliases['tables'], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + offline_user: 'offlineUser', +} as const; + +function __withTableAccessorAliases( + target: T, + freeze = false +): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & + Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + tableAccessorAliases + )) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl['db']; +export type DbView = __DbViewBase & { + /** @deprecated Use `offlineUser` instead. This alias will be removed in the next major version. */ + readonly offline_user: __DbViewBase['offlineUser']; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `offlineUser` instead. This alias will be removed in the next major version. */ + readonly offline_user: __TablesBase['offlineUser']; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = - __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases( + tablesBase, + true +) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( @@ -142,17 +197,25 @@ export const reducers = __convertToAccessorMap( ); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit< + __EventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface< - typeof REMOTE_MODULE ->; +export type ReducerEventContext = Omit< + __ReducerEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface< - typeof REMOTE_MODULE ->; +export type SubscriptionEventContext = Omit< + __SubscriptionEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit< + __ErrorContextInterface, + 'db' +> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -166,6 +229,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { return new DbConnectionBuilder( diff --git a/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts b/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts index 1d6ae15bf69..a0beb693388 100644 --- a/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts @@ -118,23 +118,78 @@ const reducersSchema = __reducers( /** 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(); +type __SchemaWithTableAccessorAliases = Omit< + typeof tablesSchema.schemaType, + 'tables' +> & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `offlineUser` instead. This alias will be removed in the next major version. */ + readonly offline_user: Omit< + (typeof tablesSchema.schemaType.tables)['offlineUser'], + 'accessorName' + > & { readonly accessorName: 'offline_user' }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: '2.0.0' as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType + .tables as __SchemaWithTableAccessorAliases['tables'], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + offline_user: 'offlineUser', +} as const; + +function __withTableAccessorAliases( + target: T, + freeze = false +): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & + Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + tableAccessorAliases + )) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl['db']; +export type DbView = __DbViewBase & { + /** @deprecated Use `offlineUser` instead. This alias will be removed in the next major version. */ + readonly offline_user: __DbViewBase['offlineUser']; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `offlineUser` instead. This alias will be removed in the next major version. */ + readonly offline_user: __TablesBase['offlineUser']; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = - __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases( + tablesBase, + true +) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( @@ -142,17 +197,25 @@ export const reducers = __convertToAccessorMap( ); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit< + __EventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface< - typeof REMOTE_MODULE ->; +export type ReducerEventContext = Omit< + __ReducerEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface< - typeof REMOTE_MODULE ->; +export type SubscriptionEventContext = Omit< + __SubscriptionEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit< + __ErrorContextInterface, + 'db' +> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -166,6 +229,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { return new DbConnectionBuilder( diff --git a/crates/bindings-typescript/tests/client_query.test.ts b/crates/bindings-typescript/tests/client_query.test.ts index 9da4eaee804..3e3b225ac6e 100644 --- a/crates/bindings-typescript/tests/client_query.test.ts +++ b/crates/bindings-typescript/tests/client_query.test.ts @@ -4,6 +4,15 @@ import { and, not, or, toSql } from '../src/lib/query'; import { tables } from '../test-app/src/module_bindings'; describe('ClientQuery.toSql', () => { + it('keeps deprecated snake_case table aliases working', () => { + assertType(tables.unindexed_player); + expect(tables.unindexed_player).toBe(tables.unindexedPlayer); + + const sql = toSql(tables.unindexed_player.build()); + + expect(sql).toBe('SELECT * FROM "unindexed_player"'); + }); + it('renders a full-table scan when no filters are applied', () => { const sql = toSql(tables.player.build()); diff --git a/crates/bindings-typescript/tests/db_connection.test.ts b/crates/bindings-typescript/tests/db_connection.test.ts index b2b29885f9d..2a1a3efc0a4 100644 --- a/crates/bindings-typescript/tests/db_connection.test.ts +++ b/crates/bindings-typescript/tests/db_connection.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test } from 'vitest'; +import { assertType, beforeEach, describe, expect, test } from 'vitest'; import { BinaryWriter, ConnectionId, @@ -149,6 +149,19 @@ function encodeMyUserProcedural(value: MyUserViewRow): Uint8Array { } describe('DbConnection', () => { + test('keeps deprecated snake_case db aliases working', async () => { + const client = DbConnection.builder() + .withUri('ws://127.0.0.1:1234') + .withDatabaseName('db') + .withWSFn(() => Promise.reject(new Error('expected test failure'))) + .build(); + + assertType(client.db.unindexed_player); + expect(client.db.unindexed_player).toBe(client.db.unindexedPlayer); + + await client['wsPromise']; + }); + test('call onConnectError callback after websocket connection failed to be established', async () => { const onConnectErrorPromise = new Deferred(); diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 1f326044fb0..3f8b21b5d32 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -27,6 +27,10 @@ use spacetimedb_lib::version::spacetimedb_lib_version; const INDENT: &str = " "; +fn ts_string_literal(s: &str) -> String { + serde_json::to_string(s).expect("serializing a string literal cannot fail") +} + pub struct TypeScript; impl Lang for TypeScript { @@ -196,10 +200,16 @@ impl Lang for TypeScript { writeln!(out, "/** 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. */"); writeln!(out, "const tablesSchema = __schema({{"); out.indent(1); + let mut table_accessor_aliases = Vec::new(); + let mut table_accessor_names = BTreeSet::new(); for table in iter_tables(module, options.visibility) { let type_ref = table.product_type_ref; let table_accessor = table.accessor_name.deref().to_case(Case::Camel); let table_name_pascalcase = table.accessor_name.deref().to_case(Case::Pascal); + table_accessor_names.insert(table_accessor.clone()); + if table.accessor_name.deref() != table_accessor { + table_accessor_aliases.push((table.accessor_name.to_string(), table_accessor.clone())); + } writeln!(out, "{table_accessor}: __table({{"); out.indent(1); write_table_opts( @@ -218,6 +228,10 @@ impl Lang for TypeScript { let type_ref = view.product_type_ref; let view_accessor = view.accessor_name.deref().to_case(Case::Camel); let view_name_pascalcase = view.accessor_name.deref().to_case(Case::Pascal); + table_accessor_names.insert(view_accessor.clone()); + if view.accessor_name.deref() != view_accessor { + table_accessor_aliases.push((view.accessor_name.to_string(), view_accessor.clone())); + } writeln!(out, "{view_accessor}: __table({{"); out.indent(1); write_table_opts(module, out, type_ref, &view.name, iter::empty(), iter::empty(), false); @@ -227,6 +241,9 @@ impl Lang for TypeScript { out.dedent(1); writeln!(out, "}});"); + table_accessor_aliases.retain(|(deprecated_accessor, _)| !table_accessor_names.contains(deprecated_accessor)); + let has_table_accessor_aliases = !table_accessor_aliases.is_empty(); + writeln!(out); writeln!(out, "/** 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. */"); writeln!(out, "const reducersSchema = __reducers("); @@ -260,6 +277,34 @@ impl Lang for TypeScript { out.dedent(1); writeln!(out, ");"); + if has_table_accessor_aliases { + writeln!(out); + writeln!( + out, + "type __SchemaWithTableAccessorAliases = Omit & {{" + ); + out.indent(1); + writeln!(out, "tables: typeof tablesSchema.schemaType.tables & {{"); + out.indent(1); + for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + writeln!( + out, + "/** @deprecated Use `{canonical_accessor}` instead. This alias will be removed in the next major version. */" + ); + writeln!( + out, + "readonly {}: Omit & {{ readonly accessorName: {} }};", + ts_string_literal(deprecated_accessor), + ts_string_literal(canonical_accessor), + ts_string_literal(deprecated_accessor) + ); + } + out.dedent(1); + writeln!(out, "}};"); + out.dedent(1); + writeln!(out, "}};"); + } + writeln!(out); writeln!( out, @@ -272,25 +317,144 @@ impl Lang for TypeScript { writeln!(out, "cliVersion: \"{}\" as const,", spacetimedb_lib_version()); out.dedent(1); writeln!(out, "}},"); - writeln!(out, "tables: tablesSchema.schemaType.tables,"); + if has_table_accessor_aliases { + writeln!( + out, + "tables: tablesSchema.schemaType.tables as __SchemaWithTableAccessorAliases[\"tables\"]," + ); + } else { + writeln!(out, "tables: tablesSchema.schemaType.tables,"); + } writeln!(out, "reducers: reducersSchema.reducersType.reducers,"); writeln!(out, "...proceduresSchema,"); out.dedent(1); writeln!(out, "}} satisfies __RemoteModule<"); out.indent(1); - writeln!(out, "typeof tablesSchema.schemaType,"); + if has_table_accessor_aliases { + writeln!(out, "__SchemaWithTableAccessorAliases,"); + } else { + writeln!(out, "typeof tablesSchema.schemaType,"); + } writeln!(out, "typeof reducersSchema.reducersType,"); writeln!(out, "typeof proceduresSchema"); out.dedent(1); writeln!(out, ">;"); out.dedent(1); + if has_table_accessor_aliases { + writeln!(out); + writeln!(out, "const tableAccessorAliases = {{"); + out.indent(1); + for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + writeln!( + out, + "{}: {},", + ts_string_literal(deprecated_accessor), + ts_string_literal(canonical_accessor) + ); + } + out.dedent(1); + writeln!(out, "}} as const;"); + + writeln!(out); + writeln!( + out, + "function __withTableAccessorAliases(target: T, freeze = false): T {{" + ); + out.indent(1); + writeln!( + out, + "const out = Object.create(Object.getPrototypeOf(target)) as T & Record;" + ); + writeln!( + out, + "Object.defineProperties(out, Object.getOwnPropertyDescriptors(target));" + ); + writeln!( + out, + "for (const [deprecatedAccessor, canonicalAccessor] of Object.entries(tableAccessorAliases)) {{" + ); + out.indent(1); + writeln!(out, "if (deprecatedAccessor in out) {{"); + out.indent(1); + writeln!(out, "continue;"); + out.dedent(1); + writeln!(out, "}}"); + writeln!(out, "Object.defineProperty(out, deprecatedAccessor, {{"); + out.indent(1); + writeln!(out, "enumerable: true,"); + writeln!(out, "configurable: false,"); + writeln!(out, "get: () => out[canonicalAccessor],"); + out.dedent(1); + writeln!(out, "}});"); + out.dedent(1); + writeln!(out, "}}"); + writeln!(out, "return freeze ? Object.freeze(out) : out;"); + out.dedent(1); + writeln!(out, "}}"); + + writeln!(out); + writeln!( + out, + "type __DbViewBase = __DbConnectionImpl[\"db\"];" + ); + writeln!(out, "export type DbView = __DbViewBase & {{"); + out.indent(1); + for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + writeln!( + out, + "/** @deprecated Use `{canonical_accessor}` instead. This alias will be removed in the next major version. */" + ); + writeln!( + out, + "readonly {}: __DbViewBase[{}];", + ts_string_literal(deprecated_accessor), + ts_string_literal(canonical_accessor) + ); + } + out.dedent(1); + writeln!(out, "}};"); + + writeln!(out); + writeln!( + out, + "type __TablesBase = __QueryBuilder;" + ); + writeln!(out, "export type Tables = __TablesBase & {{"); + out.indent(1); + for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + writeln!( + out, + "/** @deprecated Use `{canonical_accessor}` instead. This alias will be removed in the next major version. */" + ); + writeln!( + out, + "readonly {}: __TablesBase[{}];", + ts_string_literal(deprecated_accessor), + ts_string_literal(canonical_accessor) + ); + } + out.dedent(1); + writeln!(out, "}};"); + } + writeln!(out); writeln!(out, "/** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */"); - writeln!( - out, - "export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType);" - ); + if has_table_accessor_aliases { + writeln!( + out, + "const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType);" + ); + writeln!( + out, + "export const tables: Tables = __withTableAccessorAliases(tablesBase, true) as Tables;" + ); + } else { + writeln!( + out, + "export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType);" + ); + } writeln!(out); writeln!(out, "/** The reducers available in this remote SpacetimeDB module. */"); writeln!( @@ -313,31 +477,59 @@ impl Lang for TypeScript { out, "/** The context type returned in callbacks for all possible events. */" ); - writeln!( - out, - "export type EventContext = __EventContextInterface;" - ); + if has_table_accessor_aliases { + writeln!( + out, + "export type EventContext = Omit<__EventContextInterface, \"db\"> & {{ db: DbView }};" + ); + } else { + writeln!( + out, + "export type EventContext = __EventContextInterface;" + ); + } writeln!(out, "/** The context type returned in callbacks for reducer events. */"); - writeln!( - out, - "export type ReducerEventContext = __ReducerEventContextInterface;" - ); + if has_table_accessor_aliases { + writeln!( + out, + "export type ReducerEventContext = Omit<__ReducerEventContextInterface, \"db\"> & {{ db: DbView }};" + ); + } else { + writeln!( + out, + "export type ReducerEventContext = __ReducerEventContextInterface;" + ); + } writeln!( out, "/** The context type returned in callbacks for subscription events. */" ); - writeln!( - out, - "export type SubscriptionEventContext = __SubscriptionEventContextInterface;" - ); + if has_table_accessor_aliases { + writeln!( + out, + "export type SubscriptionEventContext = Omit<__SubscriptionEventContextInterface, \"db\"> & {{ db: DbView }};" + ); + } else { + writeln!( + out, + "export type SubscriptionEventContext = __SubscriptionEventContextInterface;" + ); + } writeln!(out, "/** The context type returned in callbacks for error events. */"); - writeln!( - out, - "export type ErrorContext = __ErrorContextInterface;" - ); + if has_table_accessor_aliases { + writeln!( + out, + "export type ErrorContext = Omit<__ErrorContextInterface, \"db\"> & {{ db: DbView }};" + ); + } else { + writeln!( + out, + "export type ErrorContext = __ErrorContextInterface;" + ); + } writeln!(out, "/** The subscription handle type to manage active subscriptions created from a {{@link SubscriptionBuilder}}. */"); writeln!( @@ -372,6 +564,22 @@ impl Lang for TypeScript { "export class DbConnection extends __DbConnectionImpl {{" ); out.indent(1); + if has_table_accessor_aliases { + writeln!(out, "declare db: DbView;"); + + writeln!(out); + writeln!( + out, + "constructor(config: __DbConnectionConfig) {{" + ); + out.indent(1); + writeln!(out, "super(config);"); + writeln!(out, "this.db = __withTableAccessorAliases(this.db) as DbView;"); + out.dedent(1); + writeln!(out, "}}"); + + writeln!(out); + } writeln!(out, "/** Creates a new {{@link DbConnectionBuilder}} to configure and connect to the remote SpacetimeDB instance. */"); writeln!(out, "static builder = (): DbConnectionBuilder => {{"); out.indent(1); diff --git a/crates/codegen/tests/codegen.rs b/crates/codegen/tests/codegen.rs index 7eda97c0a99..0b2c4e483f2 100644 --- a/crates/codegen/tests/codegen.rs +++ b/crates/codegen/tests/codegen.rs @@ -53,4 +53,10 @@ fn test_typescript_table_handles_are_camel_case() { assert!(!index.contains("logged_out_player: __table({")); assert!(index.contains("myPlayer: __table({")); assert!(!index.contains("my_player: __table({")); + assert!(index.contains(r#""logged_out_player": "loggedOutPlayer""#)); + assert!(index.contains(r#"readonly "logged_out_player": __TablesBase["loggedOutPlayer"];"#)); + assert!(index.contains(r#"readonly "logged_out_player": __DbViewBase["loggedOutPlayer"];"#)); + assert!(index.contains( + r#"/** @deprecated Use `loggedOutPlayer` instead. This alias will be removed in the next major version. */"# + )); } diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index aa977c6a8ac..3bc901a1f8c 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -286,22 +286,83 @@ const proceduresSchema = __procedures( __procedureSchema("with_tx", WithTxProcedure.params, WithTxProcedure.returnType), ); +type __SchemaWithTableAccessorAliases = Omit & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `loggedOutPlayer` instead. This alias will be removed in the next major version. */ + readonly "logged_out_player": Omit & { readonly accessorName: "logged_out_player" }; + /** @deprecated Use `testD` instead. This alias will be removed in the next major version. */ + readonly "test_d": Omit & { readonly accessorName: "test_d" }; + /** @deprecated Use `testF` instead. This alias will be removed in the next major version. */ + readonly "test_f": Omit & { readonly accessorName: "test_f" }; + /** @deprecated Use `myPlayer` instead. This alias will be removed in the next major version. */ + readonly "my_player": Omit & { readonly accessorName: "my_player" }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: "2.5.0" as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType.tables as __SchemaWithTableAccessorAliases["tables"], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + "logged_out_player": "loggedOutPlayer", + "test_d": "testD", + "test_f": "testF", + "my_player": "myPlayer", +} as const; + +function __withTableAccessorAliases(target: T, freeze = false): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries(tableAccessorAliases)) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl["db"]; +export type DbView = __DbViewBase & { + /** @deprecated Use `loggedOutPlayer` instead. This alias will be removed in the next major version. */ + readonly "logged_out_player": __DbViewBase["loggedOutPlayer"]; + /** @deprecated Use `testD` instead. This alias will be removed in the next major version. */ + readonly "test_d": __DbViewBase["testD"]; + /** @deprecated Use `testF` instead. This alias will be removed in the next major version. */ + readonly "test_f": __DbViewBase["testF"]; + /** @deprecated Use `myPlayer` instead. This alias will be removed in the next major version. */ + readonly "my_player": __DbViewBase["myPlayer"]; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `loggedOutPlayer` instead. This alias will be removed in the next major version. */ + readonly "logged_out_player": __TablesBase["loggedOutPlayer"]; + /** @deprecated Use `testD` instead. This alias will be removed in the next major version. */ + readonly "test_d": __TablesBase["testD"]; + /** @deprecated Use `testF` instead. This alias will be removed in the next major version. */ + readonly "test_f": __TablesBase["testF"]; + /** @deprecated Use `myPlayer` instead. This alias will be removed in the next major version. */ + readonly "my_player": __TablesBase["myPlayer"]; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases(tablesBase, true) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); @@ -310,13 +371,13 @@ export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reduc export const procedures = __convertToAccessorMap(proceduresSchema.procedures); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit<__EventContextInterface, "db"> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; +export type ReducerEventContext = Omit<__ReducerEventContextInterface, "db"> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; +export type SubscriptionEventContext = Omit<__SubscriptionEventContextInterface, "db"> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit<__ErrorContextInterface, "db"> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -328,6 +389,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** 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)); diff --git a/demo/Blackholio/client-ts/src/module_bindings/index.ts b/demo/Blackholio/client-ts/src/module_bindings/index.ts index a05ac44a41b..60946e9b803 100644 --- a/demo/Blackholio/client-ts/src/module_bindings/index.ts +++ b/demo/Blackholio/client-ts/src/module_bindings/index.ts @@ -139,22 +139,62 @@ const reducersSchema = __reducers( const proceduresSchema = __procedures( ); +type __SchemaWithTableAccessorAliases = Omit & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `consumeEntityEvent` instead. This alias will be removed in the next major version. */ + readonly "consume_entity_event": Omit & { readonly accessorName: "consume_entity_event" }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: "2.3.0" as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType.tables as __SchemaWithTableAccessorAliases["tables"], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + "consume_entity_event": "consumeEntityEvent", +} as const; + +function __withTableAccessorAliases(target: T, freeze = false): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries(tableAccessorAliases)) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl["db"]; +export type DbView = __DbViewBase & { + /** @deprecated Use `consumeEntityEvent` instead. This alias will be removed in the next major version. */ + readonly "consume_entity_event": __DbViewBase["consumeEntityEvent"]; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `consumeEntityEvent` instead. This alias will be removed in the next major version. */ + readonly "consume_entity_event": __TablesBase["consumeEntityEvent"]; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases(tablesBase, true) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); @@ -163,13 +203,13 @@ export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reduc export const procedures = __convertToAccessorMap(proceduresSchema.procedures); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit<__EventContextInterface, "db"> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; +export type ReducerEventContext = Omit<__ReducerEventContextInterface, "db"> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; +export type SubscriptionEventContext = Omit<__SubscriptionEventContextInterface, "db"> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit<__ErrorContextInterface, "db"> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -181,6 +221,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** 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)); @@ -191,4 +238,3 @@ export class DbConnection extends __DbConnectionImpl { return new SubscriptionBuilder(this); }; } - diff --git a/templates/hangman-react-ts/src/module_bindings/index.ts b/templates/hangman-react-ts/src/module_bindings/index.ts index 1c459675772..ce35d2a32ee 100644 --- a/templates/hangman-react-ts/src/module_bindings/index.ts +++ b/templates/hangman-react-ts/src/module_bindings/index.ts @@ -127,23 +127,98 @@ const reducersSchema = __reducers( /** 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(); +type __SchemaWithTableAccessorAliases = Omit< + typeof tablesSchema.schemaType, + 'tables' +> & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `currentRound` instead. This alias will be removed in the next major version. */ + readonly current_round: Omit< + (typeof tablesSchema.schemaType.tables)['currentRound'], + 'accessorName' + > & { readonly accessorName: 'current_round' }; + /** @deprecated Use `roundResult` instead. This alias will be removed in the next major version. */ + readonly round_result: Omit< + (typeof tablesSchema.schemaType.tables)['roundResult'], + 'accessorName' + > & { readonly accessorName: 'round_result' }; + /** @deprecated Use `myProgress` instead. This alias will be removed in the next major version. */ + readonly my_progress: Omit< + (typeof tablesSchema.schemaType.tables)['myProgress'], + 'accessorName' + > & { readonly accessorName: 'my_progress' }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: '2.2.0' as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType + .tables as __SchemaWithTableAccessorAliases['tables'], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + current_round: 'currentRound', + round_result: 'roundResult', + my_progress: 'myProgress', +} as const; + +function __withTableAccessorAliases( + target: T, + freeze = false +): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & + Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + tableAccessorAliases + )) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl['db']; +export type DbView = __DbViewBase & { + /** @deprecated Use `currentRound` instead. This alias will be removed in the next major version. */ + readonly current_round: __DbViewBase['currentRound']; + /** @deprecated Use `roundResult` instead. This alias will be removed in the next major version. */ + readonly round_result: __DbViewBase['roundResult']; + /** @deprecated Use `myProgress` instead. This alias will be removed in the next major version. */ + readonly my_progress: __DbViewBase['myProgress']; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `currentRound` instead. This alias will be removed in the next major version. */ + readonly current_round: __TablesBase['currentRound']; + /** @deprecated Use `roundResult` instead. This alias will be removed in the next major version. */ + readonly round_result: __TablesBase['roundResult']; + /** @deprecated Use `myProgress` instead. This alias will be removed in the next major version. */ + readonly my_progress: __TablesBase['myProgress']; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = - __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases( + tablesBase, + true +) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( @@ -154,17 +229,25 @@ export const reducers = __convertToAccessorMap( export const procedures = __convertToAccessorMap(proceduresSchema.procedures); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit< + __EventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface< - typeof REMOTE_MODULE ->; +export type ReducerEventContext = Omit< + __ReducerEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface< - typeof REMOTE_MODULE ->; +export type SubscriptionEventContext = Omit< + __SubscriptionEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit< + __ErrorContextInterface, + 'db' +> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -178,6 +261,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { return new DbConnectionBuilder( diff --git a/templates/money-exchange-react-ts/src/module_bindings/index.ts b/templates/money-exchange-react-ts/src/module_bindings/index.ts index e5b102c7132..1d93cfac4cd 100644 --- a/templates/money-exchange-react-ts/src/module_bindings/index.ts +++ b/templates/money-exchange-react-ts/src/module_bindings/index.ts @@ -107,23 +107,88 @@ const reducersSchema = __reducers( /** 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(); +type __SchemaWithTableAccessorAliases = Omit< + typeof tablesSchema.schemaType, + 'tables' +> & { + tables: typeof tablesSchema.schemaType.tables & { + /** @deprecated Use `myAccount` instead. This alias will be removed in the next major version. */ + readonly my_account: Omit< + (typeof tablesSchema.schemaType.tables)['myAccount'], + 'accessorName' + > & { readonly accessorName: 'my_account' }; + /** @deprecated Use `myAccountChanges` instead. This alias will be removed in the next major version. */ + readonly my_account_changes: Omit< + (typeof tablesSchema.schemaType.tables)['myAccountChanges'], + 'accessorName' + > & { readonly accessorName: 'my_account_changes' }; + }; +}; + /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { cliVersion: '2.2.0' as const, }, - tables: tablesSchema.schemaType.tables, + tables: tablesSchema.schemaType + .tables as __SchemaWithTableAccessorAliases['tables'], reducers: reducersSchema.reducersType.reducers, ...proceduresSchema, } satisfies __RemoteModule< - typeof tablesSchema.schemaType, + __SchemaWithTableAccessorAliases, typeof reducersSchema.reducersType, typeof proceduresSchema >; +const tableAccessorAliases = { + my_account: 'myAccount', + my_account_changes: 'myAccountChanges', +} as const; + +function __withTableAccessorAliases( + target: T, + freeze = false +): T { + const out = Object.create(Object.getPrototypeOf(target)) as T & + Record; + Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); + for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + tableAccessorAliases + )) { + if (deprecatedAccessor in out) { + continue; + } + Object.defineProperty(out, deprecatedAccessor, { + enumerable: true, + configurable: false, + get: () => out[canonicalAccessor], + }); + } + return freeze ? Object.freeze(out) : out; +} + +type __DbViewBase = __DbConnectionImpl['db']; +export type DbView = __DbViewBase & { + /** @deprecated Use `myAccount` instead. This alias will be removed in the next major version. */ + readonly my_account: __DbViewBase['myAccount']; + /** @deprecated Use `myAccountChanges` instead. This alias will be removed in the next major version. */ + readonly my_account_changes: __DbViewBase['myAccountChanges']; +}; + +type __TablesBase = __QueryBuilder; +export type Tables = __TablesBase & { + /** @deprecated Use `myAccount` instead. This alias will be removed in the next major version. */ + readonly my_account: __TablesBase['myAccount']; + /** @deprecated Use `myAccountChanges` instead. This alias will be removed in the next major version. */ + readonly my_account_changes: __TablesBase['myAccountChanges']; +}; + /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = - __makeQueryBuilder(tablesSchema.schemaType); +const tablesBase: __TablesBase = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: Tables = __withTableAccessorAliases( + tablesBase, + true +) as Tables; /** The reducers available in this remote SpacetimeDB module. */ export const reducers = __convertToAccessorMap( @@ -134,17 +199,25 @@ export const reducers = __convertToAccessorMap( export const procedures = __convertToAccessorMap(proceduresSchema.procedures); /** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; +export type EventContext = Omit< + __EventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface< - typeof REMOTE_MODULE ->; +export type ReducerEventContext = Omit< + __ReducerEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface< - typeof REMOTE_MODULE ->; +export type SubscriptionEventContext = Omit< + __SubscriptionEventContextInterface, + 'db' +> & { db: DbView }; /** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; +export type ErrorContext = Omit< + __ErrorContextInterface, + 'db' +> & { db: DbView }; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; @@ -158,6 +231,13 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} /** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ export class DbConnection extends __DbConnectionImpl { + declare db: DbView; + + constructor(config: __DbConnectionConfig) { + super(config); + this.db = __withTableAccessorAliases(this.db) as DbView; + } + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { return new DbConnectionBuilder( From fe03de5421d9e7cdeb4362e356245f5b96758f77 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sat, 13 Jun 2026 20:07:07 -0400 Subject: [PATCH 3/4] Rename TypeScript alias target accessors --- .../src/module_bindings/index.ts | 4 +-- .../test-app/src/module_bindings/index.ts | 4 +-- .../src/module_bindings/index.ts | 4 +-- .../src/module_bindings/index.ts | 4 +-- crates/codegen/src/typescript.rs | 26 +++++++++---------- .../codegen__codegen_typescript.snap | 4 +-- .../client-ts/src/module_bindings/index.ts | 4 +-- .../src/module_bindings/index.ts | 4 +-- .../src/module_bindings/index.ts | 4 +-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts index efec18b01e5..784a995b9ed 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts @@ -168,7 +168,7 @@ function __withTableAccessorAliases( const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + for (const [deprecatedAccessor, targetAccessor] of Object.entries( tableAccessorAliases )) { if (deprecatedAccessor in out) { @@ -177,7 +177,7 @@ function __withTableAccessorAliases( Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; 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 bd9d5caf88f..524bd8d05fc 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -170,7 +170,7 @@ function __withTableAccessorAliases( const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + for (const [deprecatedAccessor, targetAccessor] of Object.entries( tableAccessorAliases )) { if (deprecatedAccessor in out) { @@ -179,7 +179,7 @@ function __withTableAccessorAliases( Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; diff --git a/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts index a0beb693388..5a7a64d1e2b 100644 --- a/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts @@ -157,7 +157,7 @@ function __withTableAccessorAliases( const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + for (const [deprecatedAccessor, targetAccessor] of Object.entries( tableAccessorAliases )) { if (deprecatedAccessor in out) { @@ -166,7 +166,7 @@ function __withTableAccessorAliases( Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; diff --git a/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts b/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts index a0beb693388..5a7a64d1e2b 100644 --- a/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts @@ -157,7 +157,7 @@ function __withTableAccessorAliases( const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + for (const [deprecatedAccessor, targetAccessor] of Object.entries( tableAccessorAliases )) { if (deprecatedAccessor in out) { @@ -166,7 +166,7 @@ function __withTableAccessorAliases( Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 3f8b21b5d32..457d8960d1a 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -286,16 +286,16 @@ impl Lang for TypeScript { out.indent(1); writeln!(out, "tables: typeof tablesSchema.schemaType.tables & {{"); out.indent(1); - for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + for (deprecated_accessor, target_accessor) in &table_accessor_aliases { writeln!( out, - "/** @deprecated Use `{canonical_accessor}` instead. This alias will be removed in the next major version. */" + "/** @deprecated Use `{target_accessor}` instead. This alias will be removed in the next major version. */" ); writeln!( out, "readonly {}: Omit & {{ readonly accessorName: {} }};", ts_string_literal(deprecated_accessor), - ts_string_literal(canonical_accessor), + ts_string_literal(target_accessor), ts_string_literal(deprecated_accessor) ); } @@ -345,12 +345,12 @@ impl Lang for TypeScript { writeln!(out); writeln!(out, "const tableAccessorAliases = {{"); out.indent(1); - for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + for (deprecated_accessor, target_accessor) in &table_accessor_aliases { writeln!( out, "{}: {},", ts_string_literal(deprecated_accessor), - ts_string_literal(canonical_accessor) + ts_string_literal(target_accessor) ); } out.dedent(1); @@ -372,7 +372,7 @@ impl Lang for TypeScript { ); writeln!( out, - "for (const [deprecatedAccessor, canonicalAccessor] of Object.entries(tableAccessorAliases)) {{" + "for (const [deprecatedAccessor, targetAccessor] of Object.entries(tableAccessorAliases)) {{" ); out.indent(1); writeln!(out, "if (deprecatedAccessor in out) {{"); @@ -384,7 +384,7 @@ impl Lang for TypeScript { out.indent(1); writeln!(out, "enumerable: true,"); writeln!(out, "configurable: false,"); - writeln!(out, "get: () => out[canonicalAccessor],"); + writeln!(out, "get: () => out[targetAccessor],"); out.dedent(1); writeln!(out, "}});"); out.dedent(1); @@ -400,16 +400,16 @@ impl Lang for TypeScript { ); writeln!(out, "export type DbView = __DbViewBase & {{"); out.indent(1); - for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + for (deprecated_accessor, target_accessor) in &table_accessor_aliases { writeln!( out, - "/** @deprecated Use `{canonical_accessor}` instead. This alias will be removed in the next major version. */" + "/** @deprecated Use `{target_accessor}` instead. This alias will be removed in the next major version. */" ); writeln!( out, "readonly {}: __DbViewBase[{}];", ts_string_literal(deprecated_accessor), - ts_string_literal(canonical_accessor) + ts_string_literal(target_accessor) ); } out.dedent(1); @@ -422,16 +422,16 @@ impl Lang for TypeScript { ); writeln!(out, "export type Tables = __TablesBase & {{"); out.indent(1); - for (deprecated_accessor, canonical_accessor) in &table_accessor_aliases { + for (deprecated_accessor, target_accessor) in &table_accessor_aliases { writeln!( out, - "/** @deprecated Use `{canonical_accessor}` instead. This alias will be removed in the next major version. */" + "/** @deprecated Use `{target_accessor}` instead. This alias will be removed in the next major version. */" ); writeln!( out, "readonly {}: __TablesBase[{}];", ts_string_literal(deprecated_accessor), - ts_string_literal(canonical_accessor) + ts_string_literal(target_accessor) ); } out.dedent(1); diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index 3bc901a1f8c..47fd5846b62 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -323,14 +323,14 @@ const tableAccessorAliases = { function __withTableAccessorAliases(target: T, freeze = false): T { const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries(tableAccessorAliases)) { + for (const [deprecatedAccessor, targetAccessor] of Object.entries(tableAccessorAliases)) { if (deprecatedAccessor in out) { continue; } Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; diff --git a/demo/Blackholio/client-ts/src/module_bindings/index.ts b/demo/Blackholio/client-ts/src/module_bindings/index.ts index 60946e9b803..81afe450da0 100644 --- a/demo/Blackholio/client-ts/src/module_bindings/index.ts +++ b/demo/Blackholio/client-ts/src/module_bindings/index.ts @@ -167,14 +167,14 @@ const tableAccessorAliases = { function __withTableAccessorAliases(target: T, freeze = false): T { const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries(tableAccessorAliases)) { + for (const [deprecatedAccessor, targetAccessor] of Object.entries(tableAccessorAliases)) { if (deprecatedAccessor in out) { continue; } Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; diff --git a/templates/hangman-react-ts/src/module_bindings/index.ts b/templates/hangman-react-ts/src/module_bindings/index.ts index ce35d2a32ee..6852d40b6fe 100644 --- a/templates/hangman-react-ts/src/module_bindings/index.ts +++ b/templates/hangman-react-ts/src/module_bindings/index.ts @@ -178,7 +178,7 @@ function __withTableAccessorAliases( const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + for (const [deprecatedAccessor, targetAccessor] of Object.entries( tableAccessorAliases )) { if (deprecatedAccessor in out) { @@ -187,7 +187,7 @@ function __withTableAccessorAliases( Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; diff --git a/templates/money-exchange-react-ts/src/module_bindings/index.ts b/templates/money-exchange-react-ts/src/module_bindings/index.ts index 1d93cfac4cd..c4bf96e2c56 100644 --- a/templates/money-exchange-react-ts/src/module_bindings/index.ts +++ b/templates/money-exchange-react-ts/src/module_bindings/index.ts @@ -152,7 +152,7 @@ function __withTableAccessorAliases( const out = Object.create(Object.getPrototypeOf(target)) as T & Record; Object.defineProperties(out, Object.getOwnPropertyDescriptors(target)); - for (const [deprecatedAccessor, canonicalAccessor] of Object.entries( + for (const [deprecatedAccessor, targetAccessor] of Object.entries( tableAccessorAliases )) { if (deprecatedAccessor in out) { @@ -161,7 +161,7 @@ function __withTableAccessorAliases( Object.defineProperty(out, deprecatedAccessor, { enumerable: true, configurable: false, - get: () => out[canonicalAccessor], + get: () => out[targetAccessor], }); } return freeze ? Object.freeze(out) : out; From d0945c828276f7339175694c2883e6ce1e228d57 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 14 Jun 2026 14:35:41 -0400 Subject: [PATCH 4/4] Regenerate case conversion TS bindings --- .../src/module_bindings/index.ts | 22 +------------------ .../src/module_bindings/person_2_table.ts | 8 +++---- .../person_at_level_2_table.ts | 8 +++---- .../src/module_bindings/player_1_table.ts | 6 ++--- 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts index 784a995b9ed..9be31ab3c68 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts +++ b/crates/bindings-typescript/case-conversion-test-client/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 2.5.0 (commit edfab0febd6b95f2749493266f39b1a4b4edcccb). +// This was generated using spacetimedb cli version 2.5.0 (commit fe03de5421d9e7cdeb4362e356245f5b96758f77). /* eslint-disable */ /* tslint:disable */ @@ -122,16 +122,6 @@ type __SchemaWithTableAccessorAliases = Omit< 'tables' > & { tables: typeof tablesSchema.schemaType.tables & { - /** @deprecated Use `player1` instead. This alias will be removed in the next major version. */ - readonly player_1: Omit< - (typeof tablesSchema.schemaType.tables)['player1'], - 'accessorName' - > & { readonly accessorName: 'player_1' }; - /** @deprecated Use `person2` instead. This alias will be removed in the next major version. */ - readonly person_2: Omit< - (typeof tablesSchema.schemaType.tables)['person2'], - 'accessorName' - > & { readonly accessorName: 'person_2' }; /** @deprecated Use `personAtLevel2` instead. This alias will be removed in the next major version. */ readonly person_at_level_2: Omit< (typeof tablesSchema.schemaType.tables)['personAtLevel2'], @@ -156,8 +146,6 @@ const REMOTE_MODULE = { >; const tableAccessorAliases = { - player_1: 'player1', - person_2: 'person2', person_at_level_2: 'personAtLevel2', } as const; @@ -185,20 +173,12 @@ function __withTableAccessorAliases( type __DbViewBase = __DbConnectionImpl['db']; export type DbView = __DbViewBase & { - /** @deprecated Use `player1` instead. This alias will be removed in the next major version. */ - readonly player_1: __DbViewBase['player1']; - /** @deprecated Use `person2` instead. This alias will be removed in the next major version. */ - readonly person_2: __DbViewBase['person2']; /** @deprecated Use `personAtLevel2` instead. This alias will be removed in the next major version. */ readonly person_at_level_2: __DbViewBase['personAtLevel2']; }; type __TablesBase = __QueryBuilder; export type Tables = __TablesBase & { - /** @deprecated Use `player1` instead. This alias will be removed in the next major version. */ - readonly player_1: __TablesBase['player1']; - /** @deprecated Use `person2` instead. This alias will be removed in the next major version. */ - readonly person_2: __TablesBase['person2']; /** @deprecated Use `personAtLevel2` instead. This alias will be removed in the next major version. */ readonly person_at_level_2: __TablesBase['personAtLevel2']; }; diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts index 5a94f4ea501..9359fb15439 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts @@ -12,10 +12,10 @@ import { import { Person3Info } from './types'; export default __t.row({ - person2Id: __t.u32().primaryKey().name('Person2Id'), - firstName: __t.string().name('FirstName'), - playerRef: __t.u32(), + person2Id: __t.u32().primaryKey().name('person_2_id'), + firstName: __t.string().name('first_name'), + playerRef: __t.u32().name('player_ref'), get personInfo() { - return Person3Info; + return Person3Info.name('person_info'); }, }); diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts index 5a94f4ea501..9359fb15439 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts @@ -12,10 +12,10 @@ import { import { Person3Info } from './types'; export default __t.row({ - person2Id: __t.u32().primaryKey().name('Person2Id'), - firstName: __t.string().name('FirstName'), - playerRef: __t.u32(), + person2Id: __t.u32().primaryKey().name('person_2_id'), + firstName: __t.string().name('first_name'), + playerRef: __t.u32().name('player_ref'), get personInfo() { - return Person3Info; + return Person3Info.name('person_info'); }, }); diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts index ac5c3648fea..fba1e5f12f3 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts @@ -12,10 +12,10 @@ import { import { Player2Status } from './types'; export default __t.row({ - player1Id: __t.u32().primaryKey().name('Player1Id'), + player1Id: __t.u32().primaryKey().name('player_1_id'), playerName: __t.string().name('player_name'), - currentLevel2: __t.u32(), + currentLevel2: __t.u32().name('current_level_2'), get status3Field() { - return Player2Status; + return Player2Status.name('status_3_field'); }, });