Skip to content

Commit e9b8876

Browse files
committed
Typescript module bindings for views
1 parent 557ba31 commit e9b8876

File tree

8 files changed

+266
-18
lines changed

8 files changed

+266
-18
lines changed

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export type Indexes<
4646
[k in keyof I]: Index<TableDef, I[k]>;
4747
};
4848

49+
export type ReadonlyIndexes<
50+
TableDef extends UntypedTableDef,
51+
I extends Record<string, UntypedIndex<keyof TableDef['columns'] & string>>,
52+
> = {
53+
[k in keyof I]: ReadonlyIndex<TableDef, I[k]>;
54+
};
55+
4956
/**
5057
* A type representing a database index, which can be either unique or ranged.
5158
*/
@@ -56,32 +63,51 @@ export type Index<
5663
? UniqueIndex<TableDef, I>
5764
: RangedIndex<TableDef, I>;
5865

66+
export type ReadonlyIndex<
67+
TableDef extends UntypedTableDef,
68+
I extends UntypedIndex<keyof TableDef['columns'] & string>,
69+
> = I['unique'] extends true
70+
? ReadonlyUniqueIndex<TableDef, I>
71+
: ReadonlyRangedIndex<TableDef, I>;
72+
73+
export interface ReadonlyUniqueIndex<
74+
TableDef extends UntypedTableDef,
75+
I extends UntypedIndex<keyof TableDef['columns'] & string>,
76+
> {
77+
find(col_val: IndexVal<TableDef, I>): RowType<TableDef> | null;
78+
}
79+
5980
/**
6081
* A type representing a unique index on a database table.
6182
* Unique indexes enforce that the indexed columns contain unique values.
6283
*/
63-
export type UniqueIndex<
84+
export interface UniqueIndex<
6485
TableDef extends UntypedTableDef,
6586
I extends UntypedIndex<keyof TableDef['columns'] & string>,
66-
> = {
67-
find(col_val: IndexVal<TableDef, I>): RowType<TableDef> | null;
87+
> extends ReadonlyUniqueIndex<TableDef, I> {
6888
delete(col_val: IndexVal<TableDef, I>): boolean;
6989
update(col_val: RowType<TableDef>): RowType<TableDef>;
70-
};
90+
}
91+
92+
export interface ReadonlyRangedIndex<
93+
TableDef extends UntypedTableDef,
94+
I extends UntypedIndex<keyof TableDef['columns'] & string>,
95+
> {
96+
filter(
97+
range: IndexScanRangeBounds<TableDef, I>
98+
): IterableIterator<RowType<TableDef>>;
99+
}
71100

72101
/**
73102
* A type representing a ranged index on a database table.
74103
* Ranged indexes allow for range queries on the indexed columns.
75104
*/
76-
export type RangedIndex<
105+
export interface RangedIndex<
77106
TableDef extends UntypedTableDef,
78107
I extends UntypedIndex<keyof TableDef['columns'] & string>,
79-
> = {
80-
filter(
81-
range: IndexScanRangeBounds<TableDef, I>
82-
): IterableIterator<RowType<TableDef>>;
108+
> extends ReadonlyRangedIndex<TableDef, I> {
83109
delete(range: IndexScanRangeBounds<TableDef, I>): number;
84-
};
110+
}
85111

86112
/**
87113
* A helper type to extract the value type of an index based on the table definition and index definition.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { register_hooks } from 'spacetime:[email protected]';
2-
import { hooks } from './runtime';
2+
import { register_hooks as register_hooks_v1_1 } from 'spacetime:[email protected]';
3+
import { hooks, hooks_v1_1 } from './runtime';
34

45
register_hooks(hooks);
6+
register_hooks_v1_1(hooks_v1_1);

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ import { MODULE_DEF } from './schema';
2929

3030
import * as _syscalls from 'spacetime:[email protected]';
3131
import type { u16, u32, ModuleHooks } from 'spacetime:[email protected]';
32+
import {
33+
ANON_VIEWS,
34+
VIEWS,
35+
type AnonymousViewCtx,
36+
type ViewCtx,
37+
} from './views';
3238

3339
const { freeze } = Object;
3440

@@ -212,6 +218,46 @@ export const hooks: ModuleHooks = {
212218
},
213219
};
214220

221+
export const hooks_v1_1: import('spacetime:[email protected]').ModuleHooks = {
222+
__call_view__(id, sender, argsBuf) {
223+
const { fn, params, returnType, returnTypeBaseSize } = VIEWS[id];
224+
const ctx: ViewCtx<any> = freeze({
225+
sender: new Identity(sender),
226+
// this is the non-readonly DbView, but the typing for the user will be
227+
// the readonly one, and if they do call mutating functions it will fail
228+
// at runtime
229+
db: getDbView(),
230+
});
231+
const args = AlgebraicType.deserializeValue(
232+
new BinaryReader(argsBuf),
233+
AlgebraicType.Product(params),
234+
MODULE_DEF.typespace
235+
);
236+
const ret = fn(ctx, args);
237+
const retBuf = new BinaryWriter(returnTypeBaseSize);
238+
AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace);
239+
return retBuf.getBuffer();
240+
},
241+
__call_view_anon__(id, argsBuf) {
242+
const { fn, params, returnType, returnTypeBaseSize } = ANON_VIEWS[id];
243+
const ctx: AnonymousViewCtx<any> = freeze({
244+
// this is the non-readonly DbView, but the typing for the user will be
245+
// the readonly one, and if they do call mutating functions it will fail
246+
// at runtime
247+
db: getDbView(),
248+
});
249+
const args = AlgebraicType.deserializeValue(
250+
new BinaryReader(argsBuf),
251+
AlgebraicType.Product(params),
252+
MODULE_DEF.typespace
253+
);
254+
const ret = fn(ctx, args);
255+
const retBuf = new BinaryWriter(returnTypeBaseSize);
256+
AlgebraicType.serializeValue(retBuf, returnType, ret, MODULE_DEF.typespace);
257+
return retBuf.getBuffer();
258+
},
259+
};
260+
215261
let DB_VIEW: DbView<any> | null = null;
216262
function getDbView() {
217263
DB_VIEW ??= makeDbView(MODULE_DEF);
@@ -464,7 +510,7 @@ function makeTableView(typespace: Typespace, table: RawTableDefV9): Table<any> {
464510
return freeze(tableView);
465511
}
466512

467-
function bsatnBaseSize(typespace: Typespace, ty: AlgebraicType): number {
513+
export function bsatnBaseSize(typespace: Typespace, ty: AlgebraicType): number {
468514
const assumedArrayLength = 4;
469515
while (ty.tag === 'Ref') ty = typespace.types[ty.value];
470516
if (ty.tag === 'Product') {

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import {
2222
type AlgebraicTypeVariants,
2323
} from '../lib/algebraic_type';
2424
import type RawScopedTypeNameV9 from '../lib/autogen/raw_scoped_type_name_v_9_type';
25+
import {
26+
defineView,
27+
type AnonymousViewFn,
28+
type ViewFn,
29+
type ViewReturnTypeBuilder,
30+
} from './views';
2531

2632
/**
2733
* The global module definition that gets populated by calls to `reducer()` and lifecycle hooks.
@@ -285,6 +291,54 @@ class Schema<S extends UntypedSchemaDef> {
285291
clientDisconnected(name, {}, fn);
286292
}
287293

294+
view<Ret extends ViewReturnTypeBuilder>(
295+
name: string,
296+
ret: Ret,
297+
fn: ViewFn<S, {}, Ret>
298+
): void;
299+
view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
300+
name: string,
301+
params: Params,
302+
ret: Ret,
303+
fn: ViewFn<S, {}, Ret>
304+
): void;
305+
view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
306+
name: string,
307+
paramsOrRet: Ret | Params,
308+
retOrFn: ViewFn<S, {}, Ret> | Ret,
309+
maybeFn?: ViewFn<S, Params, Ret>
310+
): void {
311+
if (typeof retOrFn === 'function') {
312+
defineView(name, false, {}, paramsOrRet as Ret, retOrFn);
313+
} else {
314+
defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!);
315+
}
316+
}
317+
318+
anyonymousView<Ret extends ViewReturnTypeBuilder>(
319+
name: string,
320+
ret: Ret,
321+
fn: AnonymousViewFn<S, {}, Ret>
322+
): void;
323+
anyonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
324+
name: string,
325+
params: Params,
326+
ret: Ret,
327+
fn: AnonymousViewFn<S, {}, Ret>
328+
): void;
329+
anyonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
330+
name: string,
331+
paramsOrRet: Ret | Params,
332+
retOrFn: AnonymousViewFn<S, {}, Ret> | Ret,
333+
maybeFn?: AnonymousViewFn<S, Params, Ret>
334+
): void {
335+
if (typeof retOrFn === 'function') {
336+
defineView(name, true, {}, paramsOrRet as Ret, retOrFn);
337+
} else {
338+
defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!);
339+
}
340+
}
341+
288342
clientVisibilityFilter = {
289343
sql(filter: string): void {
290344
MODULE_DEF.rowLevelSecurity.push({ sql: filter });

crates/bindings-typescript/src/server/sys.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,12 @@ declare module 'spacetime:[email protected]' {
6666
export function identity(): { __identity__: u256 };
6767
export function get_jwt_payload(connection_id: u128): Uint8Array;
6868
}
69+
70+
declare module 'spacetime:[email protected]' {
71+
export type ModuleHooks = {
72+
__call_view__(id: u32, sender: u256, args: Uint8Array): Uint8Array;
73+
__call_view_anon__(id: u32, args: Uint8Array): Uint8Array;
74+
};
75+
76+
export function register_hooks(hooks: ModuleHooks);
77+
}

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import type RawIndexDefV9 from '../lib/autogen/raw_index_def_v_9_type';
55
import type RawSequenceDefV9 from '../lib/autogen/raw_sequence_def_v_9_type';
66
import type RawTableDefV9 from '../lib/autogen/raw_table_def_v_9_type';
77
import type { AllUnique } from './constraints';
8-
import type { ColumnIndex, IndexColumns, Indexes, IndexOpts } from './indexes';
8+
import type {
9+
ColumnIndex,
10+
IndexColumns,
11+
Indexes,
12+
IndexOpts,
13+
ReadonlyIndexes,
14+
} from './indexes';
915
import { MODULE_DEF, splitName } from './schema';
1016
import {
1117
RowBuilder,
@@ -108,17 +114,25 @@ export type Table<TableDef extends UntypedTableDef> = Prettify<
108114
TableMethods<TableDef> & Indexes<TableDef, TableIndexes<TableDef>>
109115
>;
110116

111-
/**
112-
* A type representing the methods available on a table.
113-
*/
114-
export type TableMethods<TableDef extends UntypedTableDef> = {
117+
export type ReadonlyTable<TableDef extends UntypedTableDef> = Prettify<
118+
ReadonlyTableMethods<TableDef> &
119+
ReadonlyIndexes<TableDef, TableIndexes<TableDef>>
120+
>;
121+
122+
export interface ReadonlyTableMethods<TableDef extends UntypedTableDef> {
115123
/** Returns the number of rows in the TX state. */
116124
count(): bigint;
117125

118126
/** Iterate over all rows in the TX state. Rust Iterator<Item=Row> → TS IterableIterator<Row>. */
119127
iter(): IterableIterator<RowType<TableDef>>;
120128
[Symbol.iterator](): IterableIterator<RowType<TableDef>>;
129+
}
121130

131+
/**
132+
* A type representing the methods available on a table.
133+
*/
134+
export interface TableMethods<TableDef extends UntypedTableDef>
135+
extends ReadonlyTableMethods<TableDef> {
122136
/**
123137
* Insert and return the inserted row (auto-increment fields filled).
124138
*
@@ -130,7 +144,7 @@ export type TableMethods<TableDef extends UntypedTableDef> = {
130144

131145
/** Delete a row equal to `row`. Returns true if something was deleted. */
132146
delete(row: RowType<TableDef>): boolean;
133-
};
147+
}
134148

135149
/**
136150
* Represents a handle to a database table, including its name, row type, and row spacetime type.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type {
2+
AlgebraicType,
3+
AlgebraicTypeVariants,
4+
ProductType,
5+
} from '../lib/algebraic_type';
6+
import type { Identity } from '../lib/identity';
7+
import type { ParamsObj } from './reducers';
8+
import { bsatnBaseSize } from './runtime';
9+
import { MODULE_DEF, type UntypedSchemaDef } from './schema';
10+
import type { ReadonlyTable } from './table';
11+
import type { Infer, InferTypeOfRow, TypeBuilder } from './type_builders';
12+
13+
export type ViewCtx<S extends UntypedSchemaDef> = Readonly<{
14+
sender: Identity;
15+
db: ReadonlyDbView<S>;
16+
}>;
17+
18+
export type AnonymousViewCtx<S extends UntypedSchemaDef> = Readonly<{
19+
db: ReadonlyDbView<S>;
20+
}>;
21+
22+
export type ReadonlyDbView<SchemaDef extends UntypedSchemaDef> = {
23+
readonly [Tbl in SchemaDef['tables'][number] as Tbl['name']]: ReadonlyTable<Tbl>;
24+
};
25+
26+
export type ViewFn<
27+
S extends UntypedSchemaDef,
28+
Params extends ParamsObj,
29+
Ret extends ViewReturnTypeBuilder,
30+
> = (ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>;
31+
32+
export type AnonymousViewFn<
33+
S extends UntypedSchemaDef,
34+
Params extends ParamsObj,
35+
Ret extends ViewReturnTypeBuilder,
36+
> = (ctx: AnonymousViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>;
37+
38+
export type ViewReturnTypeBuilder = TypeBuilder<
39+
any,
40+
| { tag: 'Array'; value: AlgebraicTypeVariants.Product }
41+
| AlgebraicTypeVariants.Product
42+
>;
43+
44+
export function defineView<
45+
S extends UntypedSchemaDef,
46+
const Anonymous extends boolean,
47+
Params extends ParamsObj,
48+
Ret extends ViewReturnTypeBuilder,
49+
>(
50+
name: string,
51+
anon: Anonymous,
52+
params: Params,
53+
ret: Ret,
54+
fn: Anonymous extends true
55+
? AnonymousViewFn<S, Params, Ret>
56+
: ViewFn<S, Params, Ret>
57+
) {
58+
const paramType = {
59+
elements: Object.entries(params).map(([n, c]) => ({
60+
name: n,
61+
algebraicType: c.algebraicType,
62+
})),
63+
};
64+
65+
MODULE_DEF.miscExports.push({
66+
tag: 'View',
67+
value: {
68+
name,
69+
isPublic: true,
70+
isAnonymous: anon,
71+
params: paramType,
72+
returnType: ret.algebraicType,
73+
},
74+
});
75+
76+
(anon ? ANON_VIEWS : VIEWS).push({
77+
fn,
78+
params: paramType,
79+
returnType: ret.algebraicType,
80+
returnTypeBaseSize: bsatnBaseSize(MODULE_DEF.typespace, ret.algebraicType),
81+
});
82+
}
83+
84+
type ViewInfo<F> = {
85+
fn: F;
86+
params: ProductType;
87+
returnType: AlgebraicType;
88+
returnTypeBaseSize: number;
89+
};
90+
91+
export const VIEWS: ViewInfo<ViewFn<any, any, any>>[] = [];
92+
export const ANON_VIEWS: ViewInfo<AnonymousViewFn<any, any, any>>[] = [];

modules/sdk-test-ts/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,3 +1015,8 @@ spacetimedb.reducer(
10151015
}
10161016
}
10171017
);
1018+
1019+
const X = t.object('X', { a: t.i32() });
1020+
spacetimedb.view('a', X, () => {
1021+
return { a: 1 };
1022+
});

0 commit comments

Comments
 (0)