Relationships
Model one-to-many associations between Rayfin entities with @one and @many, and work around the lack of native many-to-many support.
Rayfin supports one-to-many (and its inverse, many-to-one) relationships between
entities using the @one() and @many() navigation decorators. Rayfin generates the
foreign key column for you — you describe the relationship, not the join.
Many-to-many relationships are not supported natively. This page covers both the supported one-to-many pattern and the explicit join-entity workaround for many-to-many.
@one() and @many()
@one(() => Target)on the "many" side declares a many-to-one reference to a single related record. Rayfin auto-generates a{property}_idforeign key column for it.@many(() => Target)on the "one" side declares the inverse: a collection of related records. It does not generate a column — it is a read-only navigation back to the rows whose@one()field points at this record.
import { entity, authenticated, uuid, text, boolean, date, many } from '@microsoft/rayfin-core';
import { Note } from './Note.js';
@entity()
@authenticated('*', {
policy: (claims, item) => claims.sub.eq(item.user_id),
})
export class Notebook {
@uuid() id!: string;
@text({ max: 100 }) name!: string;
@boolean({ default: false }) isDefault!: boolean;
@date() createdAt!: Date;
@many(() => Note) notes?: Note[];
@text({ max: 128 }) user_id!: string;
}import { entity, authenticated, uuid, text, date, one } from '@microsoft/rayfin-core';
import { Notebook } from './Notebook.js';
@entity()
@authenticated('*', {
policy: (claims, item) => claims.sub.eq(item.user_id),
})
export class Note {
@uuid() id!: string;
@text({ max: 200 }) title!: string;
@text({ max: 10000 }) content!: string;
@date() createdAt!: Date;
@uuid() notebook_id!: string;
@one(() => Notebook, { optional: true }) notebook?: Notebook;
@text({ max: 128 }) user_id!: string;
}Register both in rayfin/data/schema.ts — see
Modeling entities.
Lazy arrow functions
@one() and @many() both take a function that returns the target class
(() => Notebook), not the class itself. This lazy form lets two entity files reference
each other — Notebook references Note and Note references Notebook — without a
circular import failing at module-load time. The function is only called after both
modules have finished loading.
@many(() => Note) notes?: Note[]; // correct — lazy reference
@many(Note) notes?: Note[]; // wrong — evaluated immediately, breaks on circular importsUse import, not import type
Import the target entity class with a plain import, never import type:
import { Notebook } from './Notebook.js'; // correct — decorators need the runtime class
import type { Notebook } from './Notebook.js'; // wrong — erased at compile time, decorator has nothing to call@one() and @many() store the arrow function and call it at runtime to resolve the
target entity's metadata. import type is erased entirely by the TypeScript compiler, so
the arrow function would close over a name that no longer exists at runtime.
Foreign key columns
Rayfin auto-generates the foreign key column when you declare @one() — you do not need
to define it yourself. Define the FK field explicitly only when your application code
needs to read or set it directly (for example, filtering by it — see
Querying).
- Naming: when you do define it, the field must follow the
{property}_idconvention —notebook_idfor anotebooknavigation property. Custom key names are not supported;foreignKeyandtargetKeyoptions do not exist on@one()/@many(). - Type: a foreign key field referencing another entity's primary key must be declared
@uuid(), matching the type of that entity'sid. Declaring it@text()is a type mismatch with the column it references. - Auth-derived owner columns are the exception. A
user_idfield populated fromclaims.sub(the signed-in user's subject claim) is not a foreign key to another Rayfin entity — it is a plain@text()field, as shown in theNotebookandNoteexamples above.
@uuid() notebook_id!: string; // correct — FK to Notebook.id (a uuid)
@text() notebook_id!: string; // wrong — type mismatch with the referenced uuid PK
@text({ max: 128 }) user_id!: string; // correct — claims.sub is not a Rayfin entity FKOption limits on relationship decorators
@one() and @many() accept only { optional?: boolean, unique?: boolean } as their
second argument — no default, max, or the other field options described in
Field types. Mark a @one() relationship { optional: true }
when the related record may not exist, matching a ? on the property, the same nullable
pattern used for scalar fields.
@one(() => Notebook, { optional: true }) notebook?: Notebook; // a note may be unfiled
@one(() => Notebook) notebook!: Notebook; // every note must have oneMany-to-many: use an explicit join entity
Rayfin does not support many-to-many relationships directly. Model them the same way you
would in raw SQL: an explicit join entity with two @one() fields, one pointing at each
side of the relationship.
import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core';
@entity()
@authenticated('*')
export class Tag {
@uuid() id!: string;
@text({ max: 50, unique: true }) name!: string;
}import { entity, authenticated, uuid, one } from '@microsoft/rayfin-core';
import { Todo } from './Todo.js';
import { Tag } from './Tag.js';
@entity()
@authenticated('*')
export class TodoTag {
@uuid() id!: string;
@uuid() todo_id!: string;
@one(() => Todo) todo!: Todo;
@uuid() tag_id!: string;
@one(() => Tag) tag!: Tag;
}Query through the join entity rather than expecting a direct tags collection on Todo:
select TodoTag rows filtered by todo_id, with tag.name in the field selection (see
dot-path selection).
const todoTags = await client.data.TodoTag.select(['id', 'tag.id', 'tag.name'])
.where({ todo_id: { eq: todoId } })
.execute();In my Rayfin project, add tagging support to the Todo entity. Create a new Tag entity in
rayfin/data/Tag.ts with a unique "name" field (text, max 50). Create a join entity
TodoTag in rayfin/data/TodoTag.ts with a uuid id, a todo_id foreign key with a @one()
reference to Todo, and a tag_id foreign key with a @one() reference to Tag. Register both
new entities in rayfin/data/schema.ts, then apply the schema with `rayfin up`.Field types
Complete reference for Rayfin's field decorators — @uuid, @text, @int, @decimal, @boolean, @date, @email, @set, and @blob — and the options each accepts.
Permissions and row-level security
Secure Rayfin entities with @role, @anonymous, and @authenticated, including row-level policies, field visibility, action-specific rules, and multi-tenant scoping.