Skip to content

Commit f7a7693

Browse files
committed
TypeScript: server module runtime API for module mounts
1 parent 50ab365 commit f7a7693

11 files changed

Lines changed: 853 additions & 70 deletions

File tree

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ConnectionId } from './connection_id';
22
import { Identity } from './identity';
33
import type { ColumnIndex, IndexColumns, IndexOpts } from './indexes';
44
import type { UntypedSchemaDef } from './schema';
5+
import type { UntypedTableDef } from './table';
56
import type { UntypedTableSchema } from './table_schema';
67
import { Timestamp } from './timestamp';
78
import type {
@@ -222,6 +223,25 @@ export type QueryBuilder<SchemaDef extends UntypedSchemaDef> = {
222223
> as Tbl['accessorName']]: TableRef<Tbl> & From<Tbl>;
223224
} & {};
224225

226+
/**
227+
* The type of `q.from` in an `addQuery` callback.
228+
*
229+
* Root-level tables appear as direct properties (same as `QueryBuilder`).
230+
* Declared namespaces appear as sub-objects — each is itself a `QueryBuilder` for that
231+
* namespace's schema, so `q.from.<namespace>.<table>` is fully typed.
232+
*
233+
* When `SchemaDef['namespaces']` is absent or `{}`, no namespace properties appear —
234+
* accessing an undeclared namespace is a compile error.
235+
*/
236+
export type SubscriptionFromBuilder<SchemaDef extends UntypedSchemaDef> =
237+
QueryBuilder<SchemaDef> & {
238+
readonly [NS in keyof NonNullable<SchemaDef['namespaces']>]: NonNullable<
239+
SchemaDef['namespaces']
240+
>[NS] extends UntypedSchemaDef
241+
? QueryBuilder<NonNullable<SchemaDef['namespaces']>[NS]>
242+
: never;
243+
};
244+
225245
/**
226246
* A runtime reference to a table. This materializes the RowExpr for us.
227247
* TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work.
@@ -334,6 +354,38 @@ export function makeQueryBuilder<SchemaDef extends UntypedSchemaDef>(
334354
return Object.freeze(qb) as QueryBuilder<SchemaDef>;
335355
}
336356

357+
/**
358+
* Builds the `q.from` object for use in `addQuery` callbacks.
359+
*
360+
* Tables whose `sourceName` contains no `.` are placed at the root.
361+
* Tables with a dotted `sourceName` (e.g. `"namespace.table"`) are grouped under a
362+
* sub-object keyed by the namespace alias, with the part after the dot as the
363+
* property key within that namespace.
364+
*/
365+
export function makeFromBuilder<SchemaDef extends UntypedSchemaDef>(
366+
tables: SchemaDef['tables']
367+
): SubscriptionFromBuilder<SchemaDef> {
368+
const result: Record<string, unknown> = Object.create(null);
369+
const namespaces: Record<string, Record<string, unknown>> = Object.create(null);
370+
371+
for (const table of Object.values(tables) as UntypedTableDef[]) {
372+
const dotIdx = table.sourceName.indexOf('.');
373+
if (dotIdx === -1) {
374+
result[table.accessorName] = createTableRefFromDef(table as any);
375+
} else {
376+
const ns = table.sourceName.slice(0, dotIdx);
377+
const key = table.sourceName.slice(dotIdx + 1);
378+
(namespaces[ns] ??= Object.create(null))[key] = createTableRefFromDef(table as any);
379+
}
380+
}
381+
382+
for (const [ns, nsTables] of Object.entries(namespaces)) {
383+
result[ns] = Object.freeze(nsTables);
384+
}
385+
386+
return Object.freeze(result) as unknown as SubscriptionFromBuilder<SchemaDef>;
387+
}
388+
337389
function createRowExpr<TableDef extends TypedTableDef>(
338390
tableDef: TableDef
339391
): RowExpr<TableDef> {
@@ -873,7 +925,10 @@ function literalValueToSql(value: unknown): string {
873925
}
874926

875927
function quoteIdentifier(name: string): string {
876-
return `"${name.replace(/"/g, '""')}"`;
928+
return name
929+
.split('.')
930+
.map(part => `"${part.replace(/"/g, '""')}"`)
931+
.join('.');
877932
}
878933

879934
function isLiteralExpr<Value>(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export interface JwtClaims {
9898
readonly fullPayload: JsonObject;
9999
}
100100

101+
export type AliasViews<SchemaDef extends UntypedSchemaDef> =
102+
SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
103+
? { readonly [K in keyof NS]: ReducerCtx<NS[K]> }
104+
: {};
105+
101106
/**
102107
* Reducer context parametrized by the inferred Schema
103108
*/
@@ -113,4 +118,5 @@ export type ReducerCtx<SchemaDef extends UntypedSchemaDef> = Readonly<{
113118
newUuidV4(): Uuid;
114119
newUuidV7(): Uuid;
115120
random: Random;
121+
as: AliasViews<SchemaDef>;
116122
}>;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function toPascalCase(s: string): string {
113113
*/
114114
export function toCamelCase<T extends string>(s: T): CamelCase<T> {
115115
const str = s
116-
.replace(/[-_]+/g, '_') // collapse runs to a single separator (no backtracking issue)
116+
.replace(/[-_]+/g, '_')
117117
.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
118118
return (str.charAt(0).toLowerCase() + str.slice(1)) as CamelCase<T>;
119119
}

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/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ export {
3333
type ResponseInit,
3434
} from './http';
3535
export type { HandlerContext, HttpHandlerExport } from './http';
36+
export { ScheduleAt } from '../lib/schedule_at';
3637

3738
import './polyfills'; // Ensure polyfills are loaded

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export type Procedures = Array<{
154154
}>;
155155

156156
export function callProcedure(
157-
moduleCtx: SchemaInner,
157+
procedures: Procedures,
158158
id: number,
159159
sender: Identity,
160160
connectionId: ConnectionId | null,
@@ -163,7 +163,7 @@ export function callProcedure(
163163
dbView: () => DbView<any>
164164
): Uint8Array {
165165
const { fn, deserializeArgs, serializeReturn, returnTypeBaseSize } =
166-
moduleCtx.procedures[id];
166+
procedures[id];
167167
const args = deserializeArgs(new BinaryReader(argsBuf));
168168

169169
const ctx: ProcedureCtx<UntypedSchemaDef> = new ProcedureCtxImpl(

0 commit comments

Comments
 (0)