Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion crates/bindings-typescript/src/lib/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ConnectionId } from './connection_id';
import { Identity } from './identity';
import type { ColumnIndex, IndexColumns, IndexOpts } from './indexes';
import type { UntypedSchemaDef } from './schema';
import type { UntypedTableDef } from './table';
import type { UntypedTableSchema } from './table_schema';
import { Timestamp } from './timestamp';
import type {
Expand Down Expand Up @@ -222,6 +223,25 @@ export type QueryBuilder<SchemaDef extends UntypedSchemaDef> = {
> as Tbl['accessorName']]: TableRef<Tbl> & From<Tbl>;
} & {};

/**
* The type of `q.from` in an `addQuery` callback.
*
* Root-level tables appear as direct properties (same as `QueryBuilder`).
* Declared namespaces appear as sub-objects — each is itself a `QueryBuilder` for that
* namespace's schema, so `q.from.<namespace>.<table>` is fully typed.
*
* When `SchemaDef['namespaces']` is absent or `{}`, no namespace properties appear —
* accessing an undeclared namespace is a compile error.
*/
export type SubscriptionFromBuilder<SchemaDef extends UntypedSchemaDef> =
QueryBuilder<SchemaDef> & {
readonly [NS in keyof NonNullable<SchemaDef['namespaces']>]: NonNullable<
SchemaDef['namespaces']
>[NS] extends UntypedSchemaDef
? QueryBuilder<NonNullable<SchemaDef['namespaces']>[NS]>
: never;
};

/**
* A runtime reference to a table. This materializes the RowExpr for us.
* TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work.
Expand Down Expand Up @@ -334,6 +354,38 @@ export function makeQueryBuilder<SchemaDef extends UntypedSchemaDef>(
return Object.freeze(qb) as QueryBuilder<SchemaDef>;
}

/**
* Builds the `q.from` object for use in `addQuery` callbacks.
*
* Tables whose `sourceName` contains no `.` are placed at the root.
* Tables with a dotted `sourceName` (e.g. `"namespace.table"`) are grouped under a
* sub-object keyed by the namespace alias, with the part after the dot as the
* property key within that namespace.
*/
export function makeFromBuilder<SchemaDef extends UntypedSchemaDef>(
tables: SchemaDef['tables']
): SubscriptionFromBuilder<SchemaDef> {
const result: Record<string, unknown> = Object.create(null);
const namespaces: Record<string, Record<string, unknown>> = Object.create(null);

for (const table of Object.values(tables) as UntypedTableDef[]) {
const dotIdx = table.sourceName.indexOf('.');
if (dotIdx === -1) {
result[table.accessorName] = createTableRefFromDef(table as any);
} else {
const ns = table.sourceName.slice(0, dotIdx);
const key = table.sourceName.slice(dotIdx + 1);
(namespaces[ns] ??= Object.create(null))[key] = createTableRefFromDef(table as any);
}
}

for (const [ns, nsTables] of Object.entries(namespaces)) {
result[ns] = Object.freeze(nsTables);
}

return Object.freeze(result) as unknown as SubscriptionFromBuilder<SchemaDef>;
}

function createRowExpr<TableDef extends TypedTableDef>(
tableDef: TableDef
): RowExpr<TableDef> {
Expand Down Expand Up @@ -873,7 +925,10 @@ function literalValueToSql(value: unknown): string {
}

function quoteIdentifier(name: string): string {
return `"${name.replace(/"/g, '""')}"`;
return name
.split('.')
.map(part => `"${part.replace(/"/g, '""')}"`)
.join('.');
}

function isLiteralExpr<Value>(
Expand Down
6 changes: 6 additions & 0 deletions crates/bindings-typescript/src/lib/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export interface JwtClaims {
readonly fullPayload: JsonObject;
}

export type AliasViews<SchemaDef extends UntypedSchemaDef> =
SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
? { readonly [K in keyof NS]: ReducerCtx<NS[K]> }
: {};

/**
* Reducer context parametrized by the inferred Schema
*/
Expand All @@ -113,4 +118,5 @@ export type ReducerCtx<SchemaDef extends UntypedSchemaDef> = Readonly<{
newUuidV4(): Uuid;
newUuidV7(): Uuid;
random: Random;
as: AliasViews<SchemaDef>;
}>;
2 changes: 1 addition & 1 deletion crates/bindings-typescript/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function toPascalCase(s: string): string {
*/
export function toCamelCase<T extends string>(s: T): CamelCase<T> {
const str = s
.replace(/[-_]+/g, '_') // collapse runs to a single separator (no backtracking issue)
.replace(/[-_]+/g, '_')
.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
return (str.charAt(0).toLowerCase() + str.slice(1)) as CamelCase<T>;
}
Expand Down
8 changes: 6 additions & 2 deletions crates/bindings-typescript/src/server/db_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export type ReadonlyDbView<SchemaDef extends UntypedSchemaDef> = {
readonly [Tbl in Values<
SchemaDef['tables']
> as Tbl['accessorName']]: ReadonlyTable<Tbl>;
};
} & (SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
? { readonly [K in keyof NS]: ReadonlyDbView<NS[K]> }
: {});

/**
* A type representing the database view, mapping table names to their corresponding Table handles.
Expand All @@ -18,4 +20,6 @@ export type DbView<SchemaDef extends UntypedSchemaDef> = {
readonly [Tbl in Values<
SchemaDef['tables']
> as Tbl['accessorName']]: Table<Tbl>;
};
} & (SchemaDef extends { namespaces: infer NS extends Record<string, UntypedSchemaDef> }
? { readonly [K in keyof NS]: DbView<NS[K]> }
: {});
1 change: 1 addition & 0 deletions crates/bindings-typescript/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ export {
type ResponseInit,
} from './http';
export type { HandlerContext, HttpHandlerExport } from './http';
export { ScheduleAt } from '../lib/schedule_at';

import './polyfills'; // Ensure polyfills are loaded
4 changes: 2 additions & 2 deletions crates/bindings-typescript/src/server/procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export type Procedures = Array<{
}>;

export function callProcedure(
moduleCtx: SchemaInner,
procedures: Procedures,
id: number,
sender: Identity,
connectionId: ConnectionId | null,
Expand All @@ -163,7 +163,7 @@ export function callProcedure(
dbView: () => DbView<any>
): Uint8Array {
const { fn, deserializeArgs, serializeReturn, returnTypeBaseSize } =
moduleCtx.procedures[id];
procedures[id];
const args = deserializeArgs(new BinaryReader(argsBuf));

const ctx: ProcedureCtx<UntypedSchemaDef> = new ProcedureCtxImpl(
Expand Down
Loading
Loading