Skip to content

Commit 14dd5da

Browse files
committed
Add ctx.db.<namespace>
1 parent 996a4e9 commit 14dd5da

6 files changed

Lines changed: 98 additions & 21 deletions

File tree

crates/bindings-typescript/src/lib/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export type TableNamesOf<S extends UntypedSchemaDef> = Values<
5252
*/
5353
export type UntypedSchemaDef = {
5454
tables: Record<string, UntypedTableDef>;
55+
namespaces?: Record<string, UntypedSchemaDef>;
5556
};
5657

5758
/**

crates/bindings-typescript/src/server/db_view.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export type ReadonlyDbView<SchemaDef extends UntypedSchemaDef> = {
99
readonly [Tbl in Values<
1010
SchemaDef['tables']
1111
> as Tbl['accessorName']]: ReadonlyTable<Tbl>;
12-
};
12+
} & (SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
13+
? { readonly [K in keyof NS]: ReadonlyDbView<NS[K]> }
14+
: {});
1315

1416
/**
1517
* A type representing the database view, mapping table names to their corresponding Table handles.
@@ -18,4 +20,6 @@ export type DbView<SchemaDef extends UntypedSchemaDef> = {
1820
readonly [Tbl in Values<
1921
SchemaDef['tables']
2022
> as Tbl['accessorName']]: Table<Tbl>;
21-
};
23+
} & (SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
24+
? { readonly [K in keyof NS]: DbView<NS[K]> }
25+
: {});

crates/bindings-typescript/src/server/runtime.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { type RowType, type Table, type TableMethods } from '../lib/table';
4040
import { hasOwn } from '../lib/util';
4141
import { type AnonymousViewCtx, type ViewCtx } from './views';
4242
import { isRowTypedQuery, makeQueryBuilder, toSql } from './query';
43-
import type { DbView } from './db_view';
43+
import type { DbView, ReadonlyDbView } from './db_view';
4444
import { getErrorConstructor, SenderError } from './errors';
4545
import { Range, type Bound } from './range';
4646
import { makeRandom, type Random } from './rng';
@@ -206,7 +206,7 @@ export const ReducerCtxImpl = class ReducerCtx<
206206
this.sender = sender;
207207
this.timestamp = timestamp;
208208
this.connectionId = connectionId;
209-
this.db = dbView;
209+
this.db = dbView as unknown as DbView<SchemaDef>;
210210
}
211211

212212
/** Reset the `ReducerCtx` to be used for a new transaction */
@@ -330,14 +330,19 @@ class ModuleHooksImpl implements ModuleHooks {
330330
}
331331

332332
get #dbView() {
333-
return (this.#dbView_ ??= freeze(
334-
Object.fromEntries(
335-
Object.values(this.#schema.schemaType.tables).map(table => [
336-
table.accessorName,
337-
makeTableView(this.#schema.typespace, table.tableDef),
338-
])
339-
)
340-
));
333+
if (this.#dbView_ !== undefined) return this.#dbView_;
334+
const rootTables = Object.values(this.#schema.schemaType.tables).map(
335+
table => [
336+
table.accessorName,
337+
makeTableView(this.#schema.typespace, table.tableDef),
338+
]
339+
);
340+
const mountNs = this.#schema.mountedDispatchInfos.map(dispatch => [
341+
dispatch.namespace,
342+
buildDbViewForDispatch(dispatch),
343+
]);
344+
this.#dbView_ = freeze(Object.fromEntries([...rootTables, ...mountNs])) as DbView<any>;
345+
return this.#dbView_;
341346
}
342347

343348
#getMountDbView(mountIdx: number): DbView<any> {
@@ -348,7 +353,7 @@ class ModuleHooksImpl implements ModuleHooks {
348353
accessorName,
349354
makeTableView(m.typespace, tableDef),
350355
])
351-
)
356+
) as DbView<any>
352357
));
353358
}
354359

@@ -436,7 +441,7 @@ class ModuleHooksImpl implements ModuleHooks {
436441
// this is the non-readonly DbView, but the typing for the user will be
437442
// the readonly one, and if they do call mutating functions it will fail
438443
// at runtime
439-
db: this.#dbView,
444+
db: this.#dbView as ReadonlyDbView<any>,
440445
from: makeQueryBuilder(moduleCtx.schemaType),
441446
});
442447
const args = deserializeParams(new BinaryReader(argsBuf));
@@ -460,7 +465,7 @@ class ModuleHooksImpl implements ModuleHooks {
460465
// this is the non-readonly DbView, but the typing for the user will be
461466
// the readonly one, and if they do call mutating functions it will fail
462467
// at runtime
463-
db: this.#dbView,
468+
db: this.#dbView as ReadonlyDbView<any>,
464469
from: makeQueryBuilder(moduleCtx.schemaType),
465470
});
466471
const args = deserializeParams(new BinaryReader(argsBuf));
@@ -490,14 +495,26 @@ class ModuleHooksImpl implements ModuleHooks {
490495
ConnectionId.nullIfZero(new ConnectionId(connection_id)),
491496
new Timestamp(timestamp),
492497
args,
493-
() => this.#dbView
498+
() => this.#dbView as DbView<any>
494499
);
495500
}
496501
}
497502

498503
const BINARY_WRITER = new BinaryWriter(0);
499504
const BINARY_READER = new BinaryReader(new Uint8Array());
500505

506+
function buildDbViewForDispatch(dispatch: MountedDispatchInfo): object {
507+
const tableEntries = dispatch.tables.map(({ accessorName, tableDef }) => [
508+
accessorName,
509+
makeTableView(dispatch.typespace, tableDef),
510+
]);
511+
const subNsEntries = dispatch.subDispatches.map(sub => [
512+
sub.namespace,
513+
buildDbViewForDispatch(sub),
514+
]);
515+
return freeze(Object.fromEntries([...tableEntries, ...subNsEntries]));
516+
}
517+
501518
function makeTableView(
502519
typespace: Typespace,
503520
table: RawTableDefV10

crates/bindings-typescript/src/server/schema.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
import type { UntypedTableDef } from '../lib/table';
5151

5252
export type MountedDispatchInfo = {
53+
namespace: string;
5354
reducerFns: Reducers;
5455
reducerDefs: RawReducerDefV10[];
5556
typespace: Typespace;
@@ -216,6 +217,7 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
216217
return {
217218
rawDef,
218219
dispatch: {
220+
namespace: '',
219221
reducerFns: [...this.#ctx.reducers],
220222
reducerDefs: [...this.#ctx.moduleDef.reducers],
221223
typespace: this.#ctx.moduleDef.typespace,
@@ -622,6 +624,19 @@ type ExtractTableEntries<H extends Record<string, SchemaEntry>> = {
622624
>;
623625
};
624626

627+
type ExtractMountSchemas<H extends Record<string, SchemaEntry>> = {
628+
[K in keyof H as H[K] extends { default: Schema<any> }
629+
? K
630+
: never]: H[K] extends { default: Schema<infer S extends UntypedSchemaDef> }
631+
? S
632+
: never;
633+
};
634+
635+
type SchemaDefForEntries<H extends Record<string, SchemaEntry>> =
636+
TablesToSchema<ExtractTableEntries<H>> & {
637+
namespaces: ExtractMountSchemas<H>;
638+
};
639+
625640
function isUntypedTableSchema(x: unknown): x is UntypedTableSchema {
626641
return typeof x === 'object' && x !== null && hasOwn(x, 'tableDef');
627642
}
@@ -666,8 +681,8 @@ function registerModuleExports(
666681
export function schema<const H extends Record<string, SchemaEntry>>(
667682
entries: H,
668683
moduleSettings?: ModuleSettings
669-
): Schema<TablesToSchema<ExtractTableEntries<H>>> {
670-
const ctx = new SchemaInner<TablesToSchema<ExtractTableEntries<H>>>(ctx => {
684+
): Schema<SchemaDefForEntries<H>> {
685+
const ctx = new SchemaInner<SchemaDefForEntries<H>>(ctx => {
671686
// Apply module settings.
672687
if (moduleSettings?.CASE_CONVERSION_POLICY != null) {
673688
ctx.setCaseConversionPolicy(moduleSettings.CASE_CONVERSION_POLICY);
@@ -682,6 +697,7 @@ export function schema<const H extends Record<string, SchemaEntry>>(
682697
}
683698
if (isMountedModuleNamespace(entry)) {
684699
const { rawDef, dispatch } = entry.default.buildMountForDispatch(entry);
700+
dispatch.namespace = accName;
685701
ctx.addMount({ namespace: accName, module: rawDef });
686702
ctx.mountedDispatchInfos.push(dispatch);
687703
continue;
@@ -728,7 +744,7 @@ export function schema<const H extends Record<string, SchemaEntry>>(
728744
});
729745
}
730746
}
731-
return { tables: tableSchemas } as TablesToSchema<ExtractTableEntries<H>>;
747+
return { tables: tableSchemas } as SchemaDefForEntries<H>;
732748
});
733749

734750
return new Schema(ctx);
@@ -758,7 +774,7 @@ export function merge(
758774
default: Schema<any>;
759775
};
760776
const tables: Record<string, UntypedTableDef> = {};
761-
for (const td of Object.values(libSchema.schemaType.tables)) {
777+
for (const td of Object.values(libSchema.schemaType.tables) as UntypedTableDef[]) {
762778
tables[td.accessorName] = td;
763779
}
764780
return { ...tables, ...(namedExports as Record<string, ModuleExport>) };

crates/bindings-typescript/src/server/views.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export function registerView<
186186
}
187187

188188
(anon ? ctx.anonViews : ctx.views).push({
189-
fn,
189+
fn: fn as unknown as ViewFn<any, any, any>,
190190
deserializeParams: ProductType.makeDeserializer(paramType, typespace),
191191
serializeReturn: AlgebraicType.makeSerializer(returnType, typespace),
192192
returnTypeBaseSize: bsatnBaseSize(typespace, returnType),

crates/bindings-typescript/tests/schema_mounts.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,43 @@ describe('schema mounts', () => {
262262
expect(reducerNames).toContain('authReducer');
263263
expect(reducerNames).toContain('utilReducer');
264264
});
265+
266+
it('mountedDispatchInfos carry namespace and nested namespace dispatches propagate', () => {
267+
const sessions = table(
268+
{ name: 'sessions' },
269+
{ id: t.u64().primaryKey().autoInc() }
270+
);
271+
const authSchema = schema({ sessions });
272+
const authLib = { default: authSchema };
273+
274+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
275+
const consumer = schema({ players, myauth: authLib });
276+
277+
const infos = consumer.mountedDispatchInfos;
278+
expect(infos).toHaveLength(1);
279+
expect(infos[0].namespace).toBe('myauth');
280+
expect(infos[0].tables[0].accessorName).toBe('sessions');
281+
});
282+
283+
it('nested mounts carry their own namespace on subDispatches', () => {
284+
const bazTable = table({ name: 'baz_items' }, { id: t.u32().primaryKey() });
285+
const bazSchema = schema({ bazTable });
286+
const bazLib = { default: bazSchema };
287+
288+
const sessions = table(
289+
{ name: 'sessions' },
290+
{ id: t.u64().primaryKey().autoInc() }
291+
);
292+
const authSchema = schema({ sessions, baz: bazLib });
293+
const authLib = { default: authSchema };
294+
295+
const players = table({ name: 'players' }, { id: t.u32().primaryKey() });
296+
const consumer = schema({ players, myauth: authLib });
297+
298+
const authInfo = consumer.mountedDispatchInfos[0];
299+
expect(authInfo.namespace).toBe('myauth');
300+
expect(authInfo.subDispatches).toHaveLength(1);
301+
expect(authInfo.subDispatches[0].namespace).toBe('baz');
302+
expect(authInfo.subDispatches[0].tables[0].accessorName).toBe('bazTable');
303+
});
265304
});

0 commit comments

Comments
 (0)