Skip to content
Draft
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
78 changes: 78 additions & 0 deletions crates/bindings-macro/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,38 @@ use crate::util::{check_duplicate_msg, match_meta};
pub(crate) struct ViewArgs {
name: Option<LitStr>,
accessor: Ident,
primary_key: Option<ViewPrimaryKeyArg>,
#[allow(unused)]
public: bool,
}

enum ViewPrimaryKeyArg {
Ident(Ident),
Literal(LitStr),
}

impl ViewPrimaryKeyArg {
fn name(&self) -> String {
match self {
Self::Ident(ident) => ident.unraw().to_string(),
Self::Literal(lit) => lit.value(),
}
}

fn ident(&self) -> Option<&Ident> {
match self {
Self::Ident(ident) => Some(ident),
Self::Literal(_) => None,
}
}
}

impl ViewArgs {
/// Parse `#[view(accessor = ..., public)]` where both `name` and `public` are required.
pub(crate) fn parse(input: TokenStream, func_ident: &Ident) -> syn::Result<Self> {
let mut name = None;
let mut accessor = None;
let mut primary_key = None;
let mut public = None;
syn::meta::parser(|meta| {
match_meta!(match meta {
Expand All @@ -36,6 +59,15 @@ impl ViewArgs {
check_duplicate_msg(&accessor, &meta, "`accessor` already specified")?;
accessor = Some(meta.value()?.parse()?);
}
sym::primary_key => {
check_duplicate_msg(&primary_key, &meta, "`primary_key` already specified")?;
let value = meta.value()?;
primary_key = Some(if value.peek(LitStr) {
ViewPrimaryKeyArg::Literal(value.parse()?)
} else {
ViewPrimaryKeyArg::Ident(value.parse()?)
});
}
});
Ok(())
})
Expand All @@ -51,6 +83,7 @@ impl ViewArgs {
.ok_or_else(|| syn::Error::new(Span::call_site(), "views must be `public`, e.g. `#[view(public)]`"))?;
Ok(Self {
name,
primary_key,
public: true,
accessor,
})
Expand All @@ -74,6 +107,29 @@ fn extract_impl_query_inner(ty: &syn::Type) -> Option<&syn::Type> {
None
}

/// If `ty` is a supported view return type, returns the row type `T`.
fn extract_view_return_row_type(ty: &syn::Type) -> Option<&syn::Type> {
if let Some(inner) = extract_impl_query_inner(ty) {
return Some(inner);
}

let syn::Type::Path(path) = ty else {
return None;
};

let seg = path.path.segments.last()?;
if !matches!(seg.ident.to_string().as_str(), "Vec" | "Option" | "RawQuery") {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
return None;
};
let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
return None;
};
Some(inner)
}

pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let vis = &original_function.vis;
let func_name = &original_function.sig.ident;
Expand Down Expand Up @@ -221,6 +277,24 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
};

let eff_ret_ty = &effective_ret_ty;
let primary_key_column_name = args.primary_key.as_ref().map(ViewPrimaryKeyArg::name);
let primary_key_field_check = args
.primary_key
.as_ref()
.and_then(ViewPrimaryKeyArg::ident)
.zip(extract_view_return_row_type(ret_ty))
.map(|(primary_key, row_ty)| {
quote! {
const _: () = {
fn _assert_view_primary_key_column #lt_params (__row: &#row_ty) #lt_where_clause {
let _ = &__row.#primary_key;
}
};
}
});
let primary_key_column_const = primary_key_column_name
.as_ref()
.map(|primary_key| quote! { const VIEW_PRIMARY_KEY_COLUMNS: &'static [&'static str] = &[#primary_key]; });

Ok(quote! {
#emitted_fn
Expand All @@ -243,6 +317,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
}
};

#primary_key_field_check

impl #func_name {
fn invoke(__ctx: #ctx_ty, __args: &[u8]) -> Vec<u8> {
spacetimedb::rt::ViewDispatcher::<#ctx_ty>::invoke::<_, _, _>(#func_name, __ctx, __args)
Expand All @@ -266,6 +342,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
/// The pointer for invoking this function
const INVOKE: Self::Invoke = #func_name::invoke;

#primary_key_column_const

/// The return type of this function
fn return_type(
ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder
Expand Down
9 changes: 9 additions & 0 deletions crates/bindings-typescript/src/lib/autogen/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/bindings-typescript/src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class ModuleContext {
schedules: [],
procedures: [],
views: [],
viewPrimaryKeys: [],
lifeCycleReducers: [],
caseConversionPolicy: { tag: 'SnakeCase' },
explicitNames: {
Expand All @@ -220,6 +221,12 @@ export class ModuleContext {
push(module.reducers && { tag: 'Reducers', value: module.reducers });
push(module.procedures && { tag: 'Procedures', value: module.procedures });
push(module.views && { tag: 'Views', value: module.views });
push(
module.viewPrimaryKeys && {
tag: 'ViewPrimaryKeys',
value: module.viewPrimaryKeys,
}
);
push(module.schedules && { tag: 'Schedules', value: module.schedules });
push(
module.lifeCycleReducers && {
Expand Down
11 changes: 9 additions & 2 deletions crates/bindings-typescript/src/server/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type ViewFn,
type ViewOpts,
type ViewReturnTypeBuilder,
type ValidateViewReturnPrimaryKey,
type Views,
} from './views';
import type { UntypedTableDef } from '../lib/table';
Expand Down Expand Up @@ -347,7 +348,8 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
opts: ViewOpts,
ret: Ret,
fn: F
fn: F,
..._: ValidateViewReturnPrimaryKey<Ret>
): ViewExport<F> {
return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
}
Expand Down Expand Up @@ -380,7 +382,12 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
anonymousView<
Ret extends ViewReturnTypeBuilder,
F extends AnonymousViewFn<S, {}, Ret>,
>(opts: ViewOpts, ret: Ret, fn: F): ViewExport<F> {
>(
opts: ViewOpts,
ret: Ret,
fn: F,
..._: ValidateViewReturnPrimaryKey<Ret>
): ViewExport<F> {
return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
}

Expand Down
13 changes: 13 additions & 0 deletions crates/bindings-typescript/src/server/view.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,24 @@ const spacetime = schema({

const arrayRetValue = t.array(person.rowType);
const optionalPerson = t.option(person.rowType);
const multiplePrimaryKeyViewRows = t.array(
t.row('MultiplePrimaryKeyViewRows', {
id: t.u32().primaryKey(),
name: t.string().primaryKey(),
})
);

spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
return ctx.from.person.build();
});

// @ts-expect-error views can have at most one primaryKey column on the returned row type.
spacetime.anonymousView(
{ name: 'multiplePrimaryKeyViewRows', public: true },
multiplePrimaryKeyViewRows,
() => []
);

spacetime.anonymousView(
{ name: 'optionalPerson', public: true },
optionalPerson,
Expand Down
97 changes: 97 additions & 0 deletions crates/bindings-typescript/src/server/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import type { OptionAlgebraicType } from '../lib/option';
import type { ParamsObj } from '../lib/reducers';
import { type UntypedSchemaDef } from '../lib/schema';
import {
ArrayBuilder,
OptionBuilder,
RowBuilder,
type ColumnBuilder,
type ColumnMetadata,
type Infer,
type InferSpacetimeTypeOfTypeBuilder,
type InferTypeOfRow,
type RowObj,
type TypeBuilder,
} from '../lib/type_builders';
import { bsatnBaseSize, toPascalCase } from '../lib/util';
Expand Down Expand Up @@ -90,6 +95,57 @@ export type ViewOpts = {

type FlattenedArray<T> = T extends readonly (infer E)[] ? E : never;

type ViewReturnRow<Ret extends ViewReturnTypeBuilder> =
Ret extends ArrayBuilder<infer Element>
? Element extends RowBuilder<infer Row>
? Row
: never
: Ret extends OptionBuilder<infer Value>
? Value extends RowBuilder<infer Row>
? Row
: never
: never;

type PrimaryKeyColumnNames<Row extends RowObj> = {
[K in keyof Row & string]: Row[K] extends ColumnBuilder<any, any, infer M>
? M extends { isPrimaryKey: true }
? K
: never
: never;
}[keyof Row & string];

type IsUnion<T, U = T> = [T] extends [never]
? false
: T extends any
? [U] extends [T]
? false
: true
: false;

type HasMultiplePrimaryKeys<Row extends RowObj> =
string extends PrimaryKeyColumnNames<Row>
? false
: IsUnion<PrimaryKeyColumnNames<Row>>;

type MultiplePrimaryKeyColumns<Ret extends ViewReturnTypeBuilder> =
PrimaryKeyColumnNames<ViewReturnRow<Ret>>;

type ERROR_view_return_type_can_have_at_most_one_primaryKey<
Columns extends string,
> = {
_primaryKeyColumns: Columns;
_fix: 'Remove primaryKey() from all but one column on the returned row type';
};

export type ValidateViewReturnPrimaryKey<Ret extends ViewReturnTypeBuilder> =
HasMultiplePrimaryKeys<ViewReturnRow<Ret>> extends true
? [
error: ERROR_view_return_type_can_have_at_most_one_primaryKey<
MultiplePrimaryKeyColumns<Ret>
>,
]
: [];

// // If we allowed functions to return either.
// type ViewReturn<Ret extends ViewReturnTypeBuilder> =
// | Infer<Ret>
Expand Down Expand Up @@ -163,6 +219,19 @@ export function registerView<
returnType,
});

const primaryKeyColumns = viewPrimaryKeyColumns(ret);
if (primaryKeyColumns.length > 1) {
throw new TypeError(
`View '${exportName}' can have at most one primaryKey() column on its returned row type; found ${primaryKeyColumns.join(', ')}`
);
}
if (primaryKeyColumns.length === 1) {
ctx.moduleDef.viewPrimaryKeys.push({
viewSourceName: exportName,
columns: primaryKeyColumns,
});
}

if (opts.name != null) {
ctx.moduleDef.explicitNames.entries.push({
tag: 'Function',
Expand Down Expand Up @@ -193,6 +262,34 @@ export function registerView<
});
}

function viewPrimaryKeyColumns(ret: ViewReturnTypeBuilder): string[] {
const row = viewReturnRow(ret);
if (row == null) {
return [];
}

return Object.entries(row.row)
.filter(
(
entry
): entry is [string, ColumnBuilder<any, any, ColumnMetadata<any>>] =>
entry[1].columnMetadata.isPrimaryKey === true
)
.map(([name]) => name);
}

function viewReturnRow(
ret: ViewReturnTypeBuilder
): RowBuilder<any> | undefined {
if (ret instanceof ArrayBuilder && ret.element instanceof RowBuilder) {
return ret.element;
}
if (ret instanceof OptionBuilder && ret.value instanceof RowBuilder) {
return ret.value;
}
return undefined;
}

type ViewInfo<F> = {
fn: F;
deserializeParams: Deserializer<any>;
Expand Down
Loading
Loading