@microsoft/rayfin-core
Complete decorator reference for @microsoft/rayfin-core — entities, field types, relationships, and permissions that generate Data API Builder configuration.
@microsoft/rayfin-core provides the decorators you apply to TypeScript classes in
rayfin/data/ to define your data model. Decorators store metadata at runtime via
Symbol.metadata; the Rayfin CLI reads that metadata to generate Data API Builder (DAB)
configuration when you run rayfin up or rayfin up db apply.
Installation
npm install @microsoft/rayfin-corenpm create @microsoft/rayfin@latest and rayfin init configure this for you
automatically. If you are adding Rayfin to an existing project, configure both manually.
TC39 Stage 3 decorators
Rayfin's decorators use the TC39 Stage 3 decorators proposal, not the older experimental decorators used by frameworks like NestJS or Angular.
- Never enable
experimentalDecoratorsoremitDecoratorMetadataintsconfig.json. - Include
ESNext.Decoratorsin thelibarray. - No
reflect-metadataimport is required. Metadata is stored on the class itself via the nativeSymbol.metadataobject, which the CLI and client SDKs read directly.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "ESNext.Decorators"],
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Warning
Bundlers that compile decorators to an older target silently break Rayfin's metadata.
If you see Expression expected errors on @entity() or other decorators, your build
target is too old. For Vite, set target: 'es2022' and use @vitejs/plugin-react
(esbuild) — @vitejs/plugin-react-swc does not support TC39 Stage 3 decorators
regardless of the configured target.
Class decorators
@entity()
Marks a class as a DAB entity — required on every class you want exposed through the data API. Entity settings are inferred from the class name and convention: the entity name is the kebab-case class name, and the source table is its pluralized snake_case form.
export declare function entity(name?: string): <T extends constructor<IEntity>>(
_target: T,
_context: ClassDecoratorContext<T>,
) => void;import { entity, uuid, text } from '@microsoft/rayfin-core';
@entity()
export class Todo {
@uuid() id!: string;
@text({ max: 200 }) title!: string;
}Every entity has an id field that serves as its UUID primary key. If you don't declare
id explicitly, Rayfin adds it to the schema automatically; the field is always optional
on create (the server generates one when omitted). Composite or non-id primary keys are
not supported.
Reserved entity names
Entity names become GraphQL type names, and the generated schema already defines scalar and root operation types. The CLI rejects an entity or connector entity whose name collides exactly, or whose name starts with GraphQL's introspection prefix __.
Matching is case-sensitive: Date is reserved, but TaskDate is not.
Reserved names: Any, Base64String, Boolean, Byte, ByteArray, Date, DateTime, Decimal, Duration, Float, ID, Int, LocalDate, LocalDateTime, LocalTime, Long, Mutation, Query, Short, SignedByte, Single, String, Subscription, Time, TimeSpan, UnsignedByte, UnsignedInt, UnsignedLong, UnsignedShort, URI, URL, UUID, plus any name beginning with __.
To resolve a collision, rename the class or pass an explicit safe name to @entity():
import { entity, text } from '@microsoft/rayfin-core';
@entity('DateRecord')
export class DateRecord {
@text({ max: 200 }) label!: string;
}@blob()
Marks a class as a Rayfin Storage folder configuration. This decorator is experimental and is exported from @microsoft/rayfin-core/experimental, not from the stable package root.
import { role } from '@microsoft/rayfin-core';
import {
blob,
ContentTypes,
mb,
StorageObject,
} from '@microsoft/rayfin-core/experimental';
@blob({
name: 'uploads',
onConflict: 'overwrite',
maxSize: mb(5),
allowedContentTypes: [ContentTypes.AnyImage],
})
@role('authenticated', '*', {
policy: (claims, item) => claims.sub.eq(item.owner_id),
})
export class ProfileImage extends StorageObject {}function blob(folderNameOrOptions?: string | BlobFolderOptions): <T extends constructor<unknown>>(
_target: T,
context: ClassDecoratorContext<T>,
) => void;
interface BlobFolderOptions {
name?: string;
onConflict?: 'error' | 'overwrite';
maxSize?: number | ByteSize;
allowedContentTypes?: MimeGlob[];
}Use StorageObject when a policy references server-managed intrinsic object fields such as owner_id. The user_metadata intrinsic was removed from StorageObject in 1.36; application metadata is represented by decorated fields on the folder class and sent through upload({ fields }). Removed in 1.36
Field decorators
Every field on an @entity() class needs exactly one type decorator. All of them accept a
shared set of base options, plus type-specific options.
Shared base options
| Option | Type | Default | Description |
|---|---|---|---|
optional | boolean | false | Allows NULL. Fields are required by default — combine with a TypeScript ? on the property. |
unique | boolean | false | Adds a unique constraint. |
default | matches the field's type | none | Default value for the column. |
column | string | property name | SQL column name to create or map for this field while keeping the TypeScript property as the GraphQL field name. New in 1.36 |
@text(options?) / @email(options?)
Variable-length text. @email() accepts the identical option type (TextFieldOptions) and
adds an email-format hint used by downstream validation and UI — it does not have its own
narrower option set.
| Option | Type | Default | Description |
|---|---|---|---|
max | number | dialect maximum | Maximum character length. Omitting it (or passing -1) falls back to the database's maximum, which on MSSQL is NVARCHAR(MAX) — this breaks GraphQL schema generation at deploy time. Always set max explicitly; see Known limitations. |
min | number | none | Minimum character length. |
regex | RegExp | none | Pattern the value must match. |
import { entity, uuid, text, email } from '@microsoft/rayfin-core';
@entity()
export class Author {
@uuid() id!: string;
@text({ max: 200 }) name!: string;
@text({ optional: true, max: 2000 }) bio?: string;
@email({ max: 254, unique: true }) emailAddress!: string;
}@uuid(options?)
UUID identifier — the conventional type for primary keys and foreign key columns. Accepts
only the shared base options (no type-specific ones). A foreign key column that references
another entity's @uuid() primary key must itself be @uuid() — a @text() field cannot
stand in for it (an auth-derived field like user_id sourced from claims.sub is a
@text() field, not a foreign key, so this rule doesn't apply to it).
@uuid() id!: string;
@uuid() category_id!: string; // foreign key — must match the referenced PK type@int(options?)
Whole numbers.
| Option | Type | Default | Description |
|---|---|---|---|
max / min | number | none | Value bounds. |
@decimal(options?)
Fixed-point decimal, for money and other precise numeric values.
| Option | Type | Default | Description |
|---|---|---|---|
max / min | number | none | Value bounds. |
precision | number | 18 | Total digits. Maximum 28 (the Data API Builder runtime limit). Must be specified together with scale. |
scale | number | 2 | Digits after the decimal point. Must be between 0 and precision. |
With neither option set, @decimal() defaults to DECIMAL(18,2).
@decimal() price!: number; // DECIMAL(18,2)
@decimal({ precision: 10, scale: 4 }) weight!: number; // DECIMAL(10,4)@boolean(options?)
True/false values. Base options only.
@date(options?)
ISO-8601 date/time, serialized from Date objects or ISO strings. Base options only.
@set(...)
String enum with a database check constraint. @set has three call forms, all producing
the same field behavior — pick whichever reads best at the call site:
type FieldDecoratorFn<T> = (
target: unknown,
context: ClassFieldDecoratorContext<unknown, T | undefined>,
) => void;
// 1. Values only
function set<T extends [string, ...string[]]>(...values: T): FieldDecoratorFn<T[number]>;
// 2. A single options object, with values in `enum`
function set<T extends [string, ...string[]]>(options: SetFieldOptions<T>): FieldDecoratorFn<T[number]>;
// 3. Base options object, followed by values
function set<T extends [string, ...string[]]>(
options: Omit<SetFieldOptions<T>, 'enum'>,
...values: T
): FieldDecoratorFn<T[number]>;SetFieldOptions<T> adds one required property, enum: T, on top of the shared base
options.
// Form 1 — values only
@set('todo', 'in-progress', 'done') status!: 'todo' | 'in-progress' | 'done';
// Form 2 — options object with `enum`
@set({ enum: ['todo', 'in-progress', 'done'], optional: true })
status?: 'todo' | 'in-progress' | 'done';
// Form 3 — base options, then values
@set({ optional: true }, 'todo', 'in-progress', 'done')
status?: 'todo' | 'in-progress' | 'done';Field type to database mapping
| Decorator | Logical type | Database mapping |
|---|---|---|
@uuid() | UUID | UNIQUEIDENTIFIER |
@text() | string | NVARCHAR(n), sized from max |
@int() | integer | INT |
@decimal() | decimal | DECIMAL |
@boolean() | boolean | BIT |
@date() | datetime | DATETIME2 |
@email() | string | Same as @text(), plus an email-format hint |
@set() | enum | String column with a check constraint |
Relationship decorators
@one() and @many() take a lazy () => Target function (not the class directly) so
entities in different files can reference each other without import-order issues. Both
accept only RelationshipFieldOptions — the base options minus default:
| Option | Type | Default | Description |
|---|---|---|---|
optional | boolean | false | Nullable relationship (optional foreign key). |
unique | boolean | false | Makes a @one() relationship one-to-one instead of many-to-one. |
Custom foreign key or target key names are not supported — see Known limitations.
@one(target, options?)
Many-to-one (or one-to-one with unique: true). Rayfin auto-generates a foreign key column
named {property}_id — you don't need to declare it unless your application code reads or
writes the raw ID.
import { entity, uuid, text, one } from '@microsoft/rayfin-core';
import { Category } from './Category.js'; // `import`, not `import type` — the decorator needs the runtime class
@entity()
export class Todo {
@uuid() id!: string;
@text({ max: 200 }) title!: string;
@one(() => Category) category!: Category; // generates category_id
@one(() => Category, { optional: true }) parentCategory?: Category;
}@many(target, options?)
The inverse, one-to-many side of a @one() relationship.
import { entity, uuid, text, many } from '@microsoft/rayfin-core';
import { Todo } from './Todo.js';
@entity()
export class Category {
@uuid() id!: string;
@text({ max: 100 }) name!: string;
@many(() => Todo) todos?: Todo[];
}Many-to-many relationships are not supported directly — model them with an explicit join
entity that has two @one() fields. See Known limitations.
Note
@microsoft/rayfin-core ships a built-in, read-only User system entity (id, email)
that is always a valid @one() / @many() target — you don't need to declare it in your
own schema to reference it.
Connector-only custom relationship keys
CustomKeyRelationshipFieldOptions is exported from the stable package root in 1.36 for connector entities with explicit primary keys. Data-path entities still use the conventional id primary key; sourceFields and targetFields are not available for relationships between normal rayfin/data/ entities. New in 1.36
Permission decorators
Every entity needs an explicit permission decorator. Omitting one is not the same as
denying access — it silently applies authenticated: * (full CRUD for any signed-in user),
which is rarely what you want in production.
@authenticated(actions?, options?)
Shorthand for the built-in authenticated role — requires a valid user session.
type RoleDecoratorFn<TEntity> = (
target: constructor<TEntity>,
context: ClassDecoratorContext<constructor<TEntity>>,
) => void;
function authenticated<TEntity extends object = object>(
actions?: SimpleAction | SimpleAction[], // default: '*'
options?: RoleDeclarationOptions<TEntity>,
): RoleDecoratorFn<TEntity>;| Parameter | Type | Default | Description |
|---|---|---|---|
actions | 'create' | 'read' | 'update' | 'delete' | 'execute' | '*', or an array of them | '*' | The DAB action(s) this role may perform. 'execute' applies to stored procedures. |
options.policy | (claims, item) => PolicyExpression | none | Row-level filter — see Policy DSL below. |
options.include | (keyof TEntity)[] | none | Restrict the role to only these fields. |
options.exclude | (keyof TEntity)[] | none | Hide these fields from the role. |
import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core';
@entity()
@authenticated('*', {
policy: (claims, item) => claims.sub.eq(item.user_id),
})
export class Todo {
@uuid() id!: string;
@text({ max: 200 }) title!: string;
@text({ max: 128 }) user_id!: string;
}@role(roleName, actions, options?)
The general-purpose form that @authenticated() and @anonymous() are shorthands for.
function role<TEntity extends object = object>(
roleName: 'authenticated' | 'anonymous',
actions: SimpleAction | SimpleAction[],
options?: RoleDeclarationOptions<TEntity>,
): RoleDecoratorFn<TEntity>;@anonymous(actions?, options?)
Public, unauthenticated access. Exported from the package root:
function anonymous<TEntity extends object = object>(
actions?: SimpleAction | SimpleAction[], // default: '*'
options?: RoleDeclarationOptions<TEntity>,
): RoleDecoratorFn<TEntity>;import { entity, uuid, text, anonymous, authenticated } from '@microsoft/rayfin-core';
@entity()
@anonymous('read')
@authenticated('*')
export class PublicPost {
@uuid() id!: string;
@text({ max: 200 }) title!: string;
}Note
Anonymous data access on a deployed Fabric app requires a tenant administrator to enable the "Enable anonymous data access for Fabric Apps" tenant setting. The decorators compile to DAB anonymous-role permissions; the tenant setting controls whether those permissions can be exercised in Fabric. See Permissions for the full picture.
Policy DSL and field visibility
The policy callback receives two typed arguments and returns a composable expression that
compiles to a DAB policy string:
claims— aClaimsDslexposingclaims.sub,claims.email, andclaims.role. These are the only supported claims.item— a proxy with one property per entity field, typed from the class itself so a typo fails at compile time.
Both claims.<name> and item.<field> support .eq(value) and .neq(value), each
returning a PolicyExpression. Combine expressions with .and() / .or(), which
parenthesize both sides automatically:
policy: (claims, item) =>
claims.role.eq('admin').or(claims.sub.eq(item.owner_id).and(item.isActive.eq(true)))Use include / exclude on any role decorator to control field-level visibility per
action. Both arrays are typed against the entity's actual property names:
import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core';
@entity()
@authenticated('read', { exclude: ['secret'] })
@authenticated(['create', 'update'], {
policy: (claims, item) => claims.sub.eq(item.owner_id),
})
export class Document {
@uuid() id!: string;
@text({ max: 128 }) owner_id!: string;
@text({ max: 200 }) title!: string;
@text({ optional: true, max: 5000 }) secret?: string;
}Multiple role decorators on the same class are aggregated; conflicting declarations for the same role and action produce a warning at generation time.
Experimental subpath exports
@microsoft/rayfin-core/experimental now contains the experimental Storage surface only: blob, StorageObject, bytes, kb, mb, gb, ContentTypes, and related storage option types.
Connector-oriented exports that were previously under the core experimental subpath — Source, connector-widened entity / field decorators, AutoGenerated, and the experimental connector schema types — are no longer exported there. Use the connector packages documented under Connectors for connector models. Removed in 1.36
Form validation
@microsoft/rayfin-core can build a Standard Schema
validator directly from an @entity() class, so you can validate form input without a
separate library.
function toStandardSchema<T, K extends keyof T = never>(
entity: EntityClass<T>,
options?: { omit?: readonly K[] },
): RayfinStandardSchema<Omit<T, K | 'id'>>;
function getFieldConstraints<T, K extends keyof T>(
entity: EntityClass<T>,
field: K & string,
): FieldConstraints | undefined;toStandardSchema automatically omits the id primary key and any @one() / @many()
relationship properties — pass additional field names (like timestamps) via omit.
import { toStandardSchema, getFieldConstraints } from '@microsoft/rayfin-core';
import { Todo } from '../rayfin/data/Todo.js';
const todoInputSchema = toStandardSchema(Todo, {
omit: ['createdAt', 'updatedAt'] as const,
});
const result = todoInputSchema.validate({ title: 'Write docs' });
if (result.issues) {
// result.issues: readonly StandardSchemaV1.Issue[]
} else {
// result.value is typed as Omit<Todo, 'id' | 'createdAt' | 'updatedAt'>
}
const titleConstraints = getFieldConstraints(Todo, 'title');
// { type: 'string', min: 1, max: 200, optional: false }The returned object implements the StandardSchemaV1 contract (~standard), so it works
directly with TanStack Form, Conform, tRPC v11, and any other Standard Schema–compatible
library, in addition to its own convenience .validate() method.