Skip to content

Commit 3e7ab2c

Browse files
authored
Add a typescript query builder for views (#3812)
# Description of Changes This adds a way to build queries with typescript views, and it allows views to return queries (if the return type of the query is an array). For examples and syntax, you can look at the tests in [crates/bindings-typescript/tests/query.test.ts](https://github.com/clockworklabs/SpacetimeDB/compare/jsdt/ts-query-builder?expand=1#diff-4fd25c191f1207085a491cf84996c601f805f5e8280d1cf2a812ebad6aa6e75a). To play around with the syntax, you might find it easier to look in [crates/bindings-typescript/src/server/view.test-d.ts](https://github.com/clockworklabs/SpacetimeDB/compare/jsdt/ts-query-builder?expand=1#diff-4fd25c191f1207085a491cf84996c601f805f5e8280d1cf2a812ebad6aa6e75a). This could still use some cleanup, and there are some places where the type safety is imperfect. I'll try to list the known limitations here: 1. This will allow the use of `eq` for columns that are product types, even though the query engine doesn't support it. This can be fixed later, and it would only be a breaking change for modules that have invalid queries. 2. When we check if a view is returning a query of the correct type, we are checking with the typescript row type. We should be checking with the spacetime type, since this type checking will allow a couple incorrect things to be returned: 1. A different table with any superset of the fields (for example, a different table that has one extra field). That will fail when executing, but will be allowed by the typescript compiler. 2. A table with the same fields, but with those fields in a different order would also fail at runtime, but be allowed by the typescript compiler. 4. A table with fields of a different spacetimetype that map to the same typescript type (like `u16` and `u32`). I can also add back functions for things like inequality once we are ok with the rest of it. # API and ABI breaking changes This adds some new API surface, but does not break existing code. # Expected complexity level and risk 2. # Testing For automated tests, there are unit tests to see what sql gets emitted in `tests/query.test.ts`, and some tests of the types in `view.test-d.ts`. I've also run some manual tests with a typescript module with views.
1 parent 46f3e07 commit 3e7ab2c

File tree

10 files changed

+1320
-10
lines changed

10 files changed

+1320
-10
lines changed

crates/bindings-typescript/src/lib/autogen/view_result_header_type.ts

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export type UntypedSchemaDef = {
5959
tables: readonly UntypedTableDef[];
6060
};
6161

62+
let REGISTERED_SCHEMA: UntypedSchemaDef | null = null;
63+
64+
export function getRegisteredSchema(): UntypedSchemaDef {
65+
if (REGISTERED_SCHEMA == null) {
66+
throw new Error('Schema has not been registered yet. Call schema() first.');
67+
}
68+
return REGISTERED_SCHEMA;
69+
}
70+
6271
/**
6372
* Helper type to convert an array of TableSchema into a schema definition
6473
*/
@@ -636,6 +645,16 @@ export function schema<const H extends readonly TableSchema<any, any, any>[]>(
636645
// Modify the `MODULE_DEF` which will be read by
637646
// __describe_module__
638647
MODULE_DEF.tables.push(...tableDefs);
648+
REGISTERED_SCHEMA = {
649+
tables: handles.map(handle => ({
650+
name: handle.tableName,
651+
accessorName: handle.tableName,
652+
columns: handle.rowType.row,
653+
rowType: handle.rowSpacetimeType,
654+
indexes: handle.idxs,
655+
constraints: handle.constraints,
656+
})),
657+
};
639658
// MODULE_DEF.typespace = typespace;
640659
// throw new Error(
641660
// MODULE_DEF.tables

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ type ColList = ColId[];
3434
/**
3535
* A helper type to extract the row type from a TableDef
3636
*/
37-
export type RowType<TableDef extends UntypedTableDef> = InferTypeOfRow<
38-
TableDef['columns']
39-
>;
37+
export type RowType<TableDef extends Pick<UntypedTableDef, 'columns'>> =
38+
InferTypeOfRow<TableDef['columns']>;
4039

4140
/**
4241
* Coerces a column which may be a TypeBuilder or ColumnBuilder into a ColumnBuilder

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ export type InferTypeOfRow<T extends RowObj> = {
4141
[K in keyof T & string]: InferTypeOfTypeBuilder<CollapseColumn<T[K]>>;
4242
};
4343

44+
/**
45+
* Helper type to extract the type of a row from an object.
46+
*/
47+
export type InferSpacetimeTypeOfRow<T extends RowObj> = {
48+
[K in keyof T & string]: InferSpacetimeTypeOfTypeBuilder<
49+
CollapseColumn<T[K]>
50+
>;
51+
};
52+
4453
/**
4554
* Helper type to extract the Spacetime type from a row object.
4655
*/

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ import {
2020
type TypeBuilder,
2121
} from './type_builders';
2222
import { bsatnBaseSize, toPascalCase } from './util';
23+
import { type QueryBuilder, type RowTypedQuery } from '../server/query';
2324

2425
export type ViewCtx<S extends UntypedSchemaDef> = Readonly<{
2526
sender: Identity;
2627
db: ReadonlyDbView<S>;
28+
from: QueryBuilder<S>;
2729
}>;
2830

2931
export type AnonymousViewCtx<S extends UntypedSchemaDef> = Readonly<{
3032
db: ReadonlyDbView<S>;
33+
from: QueryBuilder<S>;
3134
}>;
3235

3336
export type ReadonlyDbView<SchemaDef extends UntypedSchemaDef> = {
@@ -39,17 +42,34 @@ export type ViewOpts = {
3942
public: true;
4043
};
4144

45+
type FlattenedArray<T> = T extends readonly (infer E)[] ? E : never;
46+
47+
// // If we allowed functions to return either.
48+
// type ViewReturn<Ret extends ViewReturnTypeBuilder> =
49+
// | Infer<Ret>
50+
// | RowTypedQuery<FlattenedArray<Infer<Ret>>>;
51+
4252
export type ViewFn<
4353
S extends UntypedSchemaDef,
4454
Params extends ParamsObj,
4555
Ret extends ViewReturnTypeBuilder,
46-
> = (ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>;
56+
> =
57+
| ((ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>)
58+
| ((
59+
ctx: ViewCtx<S>,
60+
params: InferTypeOfRow<Params>
61+
) => RowTypedQuery<FlattenedArray<Infer<Ret>>>);
4762

4863
export type AnonymousViewFn<
4964
S extends UntypedSchemaDef,
5065
Params extends ParamsObj,
5166
Ret extends ViewReturnTypeBuilder,
52-
> = (ctx: AnonymousViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>;
67+
> =
68+
| ((ctx: AnonymousViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>)
69+
| ((
70+
ctx: AnonymousViewCtx<S>,
71+
params: InferTypeOfRow<Params>
72+
) => RowTypedQuery<FlattenedArray<Infer<Ret>>>);
5373

5474
export type ViewReturnTypeBuilder =
5575
| TypeBuilder<
@@ -97,6 +117,7 @@ export function defineView<
97117
},
98118
});
99119

120+
// If it is an option, we wrap the function to make the return look like an array.
100121
if (returnType.tag == 'Sum') {
101122
const originalFn = fn;
102123
fn = ((ctx: ViewCtx<S>, args: InferTypeOfRow<Params>) => {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { reducers } from '../lib/reducers';
55
export { SenderError, SpacetimeHostError, errors } from './errors';
66
export { type Reducer, type ReducerCtx } from '../lib/reducers';
77
export { type DbView } from './db_view';
8+
export { and, or, not } from './query';
89
export type { ProcedureCtx, TransactionCtx } from '../lib/procedures';
910

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

0 commit comments

Comments
 (0)