@microsoft/rayfin-guide 1.1.0 → 1.33.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ ---
2
+ sidebar_position: 1
3
+ ---
4
+
5
+ # CLI Quickstart
6
+
7
+ Use these commands to create and manage Rayfin projects.
8
+
9
+ ## Pre-requisites
10
+
11
+ Install the [prerequisites](../getting-started/index.md#prerequisites) before continuing.
12
+
13
+ ## Create a new project
14
+
15
+ ```bash
16
+ npm create @microsoft/rayfin@latest my-app
17
+ ```
18
+
19
+ The project name is a **positional argument** — not a flag.
20
+ Provide a valid directory name like `my-app`, or use `.` to scaffold into the current directory.
21
+
22
+ - The CLI prompts for a template and database dialect interactively.
23
+ - Use `-t, --template <name>` to skip the template prompt (run `--list-templates` to see options).
24
+ - Expected result: `✔ Project created`.
25
+
26
+ ## Add Rayfin to an existing project
27
+
28
+ If you already have a project and want to add Rayfin:
29
+
30
+ ```bash
31
+ npm install --save-dev @microsoft/rayfin-cli
32
+ npx rayfin init
33
+ ```
34
+
35
+ This installs the CLI and runs the interactive setup to create the `rayfin/` directory with starter configuration files.
36
+
37
+ ## Deploy to Fabric
38
+
39
+ ```bash
40
+ npx rayfin login
41
+ npx rayfin up
42
+ ```
43
+
44
+ - Sign in with your Microsoft account when prompted.
45
+ - The CLI uses the OS keychain for token storage when available.
46
+ - Deploys your project to Microsoft Fabric.
47
+ - Use `npx rayfin up status` to check deployment state.
48
+ - Expected result: a successful deployment status for your Rayfin item.
49
+
50
+ For non-interactive environments, use service principal authentication instead:
51
+
52
+ ```bash
53
+ npx rayfin login --service-principal --client-id <id> --client-secret <secret> --tenant <tenant-id>
54
+ npx rayfin up
55
+ ```
56
+
57
+ ## Apply database schema changes
58
+
59
+ ```bash
60
+ npx rayfin up db apply [--force]
61
+ ```
62
+
63
+ - Run after updating decorated entities under `rayfin/data`.
64
+ - Ensure `npx rayfin up` has completed successfully before applying.
65
+ - Use `--force` to proceed when warned about potential data loss.
66
+ - Expected result: `✔ Configuration applied successfully!`.
67
+
68
+ ## Deploy static content
69
+
70
+ If `staticHosting` is enabled in `rayfin/rayfin.yml`, `npx rayfin up` automatically builds, packages, and deploys your static assets.
71
+
72
+ When iterating locally with Vite, opt out of the static deploy phase with `npx rayfin up --exclude-services staticHosting`.
73
+ This is what the scaffolded `npm run dev` script does so the backend deploys but the local Vite server keeps serving your frontend.
74
+
75
+ To redeploy static content independently without running the full `rayfin up` flow:
76
+
77
+ ```bash
78
+ npx rayfin up staticapp deploy
79
+ ```
80
+
81
+ - Runs the configured `buildCommand`, packages the output folder into a ZIP, and uploads it.
82
+ - Use `--skip-build` to deploy existing build output without rebuilding.
83
+ - Expected result: `Static content deployed` with a hosting URL.
84
+
85
+ ## Troubleshooting
86
+
87
+ - **Authentication fails (401/403)**: Rerun auth scripts and confirm `NODE_AUTH_TOKEN` is set; verify with `npm view @microsoft/rayfin-cli version`.
88
+ - **Database apply fails**: Wait for services to report healthy, then rerun `npx rayfin up db apply`.
@@ -0,0 +1,267 @@
1
+ ---
2
+ sidebar_position: 2
3
+ ---
4
+
5
+ # Read and Write Data with the GraphQL Client
6
+
7
+ Rayfin's supports GraphQL fluent client lets you run create, read, update, and delete operations with full type safety and zero handwritten queries.
8
+ Once you have your data models defined follow the guidance below to build out the front end of your app and use the data API client to perform read, write, update, or delete data.
9
+
10
+ ## Instantiate a RayfinClient
11
+
12
+ Use `RayfinClient` to connect your frontend or Node.js service to the Rayfin backend.
13
+ The generic arguments keep strong typing across REST, GraphQL fluent, and raw GraphQL clients.
14
+
15
+ In most applications you should create a single client instance and reuse it.
16
+ This is commonly implemented as a small singleton module or service wrapper.
17
+
18
+ ```typescript
19
+ import { RayfinClient } from '@microsoft/rayfin-client';
20
+ import type { Note } from '../rayfin/data/Note';
21
+
22
+ type AppSchema = { Note: Note };
23
+
24
+ const rayfinClient = new RayfinClient<AppSchema>({
25
+ baseUrl: import.meta.env.VITE_RAYFIN_API_URL ?? 'http://localhost:5168',
26
+ publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY ?? '',
27
+ });
28
+ ```
29
+
30
+ ## Read Data with GraphQL
31
+
32
+ Rayfin's fluent client produces DAB-compliant GraphQL and returns typed entities.
33
+
34
+ The GraphQL fluent client is available as `client.data.<Entity>`.
35
+ Some older examples may show `client.data.gql.<Entity>`.
36
+
37
+ ### Read multiple records
38
+
39
+ Here is an example to read all the records and order by a column.
40
+
41
+ ```typescript
42
+ const notes = await this.rayfinClient.data.Note.select([
43
+ 'id',
44
+ 'title',
45
+ 'content',
46
+ 'contentType',
47
+ 'isPinned',
48
+ 'isArchived',
49
+ 'createdAt',
50
+ 'updatedAt',
51
+ 'user_id',
52
+ 'notebook_id',
53
+ 'notebook.id',
54
+ 'notebook.name',
55
+ 'notebook.color',
56
+ ])
57
+ .orderBy({ createdAt: 'desc' })
58
+ .execute();
59
+
60
+ // Sort pinned notes to the top
61
+ return this.sortWithPinnedFirst(notes);
62
+ ```
63
+
64
+ ### Filter records
65
+
66
+ Use `where` to filter results.
67
+
68
+ ```typescript
69
+ const pinnedNotes = await this.rayfinClient.data.Note.select([
70
+ 'id',
71
+ 'title',
72
+ 'isPinned',
73
+ ])
74
+ .where({ isPinned: { eq: true } })
75
+ .orderBy({ createdAt: 'desc' })
76
+ .execute();
77
+ ```
78
+
79
+ ### Paginate Large Lists
80
+
81
+ Use cursor pagination for large lists.
82
+
83
+ ```typescript
84
+ const page = await this.rayfinClient.data.Note.select([
85
+ 'id',
86
+ 'title',
87
+ 'createdAt',
88
+ ])
89
+ .orderBy({ createdAt: 'desc' })
90
+ .first(25)
91
+ .executePaginated();
92
+
93
+ const items = page.items;
94
+ const cursor = page.endCursor;
95
+ const hasNextPage = page.hasNextPage;
96
+ ```
97
+
98
+ ### Fetch a Single Record
99
+
100
+ Here is an example to fetch a single record.
101
+
102
+ ```typescript
103
+ const note = await rayfinClient.data.Note.findById('00000000-0000-0000-0000-000000000000');
104
+ ```
105
+
106
+ ## Create Records
107
+
108
+ Use `create` to insert a record with full validation based on your entity definition.
109
+ The returned value is the newly created entity.
110
+
111
+ ```typescript
112
+ const noteData = {
113
+ title: 'My first note',
114
+ content: 'test note',
115
+ isPinned: false,
116
+ isArchived: false,
117
+ createdAt: new Date(),
118
+ updatedAt: new Date(),
119
+ };
120
+
121
+ const newNote = await this.rayfinClient.data.Note.create(noteData);
122
+ ```
123
+
124
+ ### Creating Records with Relationships
125
+
126
+ When creating entities that have relationships (defined with `@one()`), you can pass either the full related object or just an object with the primary key.
127
+
128
+ **Option 1: Pass the full object**
129
+
130
+ ```typescript
131
+ // If you already have the full Notebook object
132
+ const notebook = await rayfinClient.data.Notebook.findFirst({ name: { eq: 'Work' } });
133
+
134
+ const note = await rayfinClient.data.Note.create({
135
+ title: 'Meeting notes',
136
+ content: 'Discussion points...',
137
+ isPinned: false,
138
+ isArchived: false,
139
+ createdAt: new Date(),
140
+ updatedAt: new Date(),
141
+ notebook: notebook, // Full object
142
+ });
143
+ ```
144
+
145
+ **Option 2: Pass an object with just the ID**
146
+
147
+ ```typescript
148
+ // If you only have the notebook ID (avoids extra fetch)
149
+ const note = await rayfinClient.data.Note.create({
150
+ title: 'Meeting notes',
151
+ content: 'Discussion points...',
152
+ isPinned: false,
153
+ isArchived: false,
154
+ createdAt: new Date(),
155
+ updatedAt: new Date(),
156
+ notebook: { id: 'notebook-id' }, // Just the primary key
157
+ });
158
+ ```
159
+
160
+ Both forms produce the same GraphQL mutation.
161
+ The second form is useful when you already know the related entity's ID and don't need to fetch the full object.
162
+
163
+ ## Update Records
164
+
165
+ Use `update` to patch records by filter.
166
+
167
+ ```typescript
168
+ const noteUpdates = {
169
+ ...updates,
170
+ updatedAt: new Date(),
171
+ };
172
+
173
+ await this.rayfinClient.data.Note.update({ id }, noteUpdates);
174
+ ```
175
+
176
+ ### Updating Records with Relationships
177
+
178
+ When updating a relationship field (defined with `@one()`), you can pass either the full related object or just an object with the primary key — the same options available when creating.
179
+
180
+ ```typescript
181
+ // Move a note to a different notebook by passing just the ID
182
+ await rayfinClient.data.Note.update(
183
+ { id: '00000000-0000-0000-0000-000000000000' },
184
+ { notebook: { id: 'new-notebook-id' } },
185
+ );
186
+ ```
187
+
188
+ The client converts the relationship object to a foreign key field (`notebook_id`) in the generated GraphQL mutation.
189
+
190
+ ## Delete Records
191
+
192
+ Use `delete` to remove records that match a filter.
193
+ The method resolves when the backend confirms deletion.
194
+
195
+ ```typescript
196
+ await this.rayfinClient.data.Note.delete({ id });
197
+ ```
198
+
199
+ ## Limitations and Workarounds
200
+
201
+ - `count()` is not implemented today; select the identifiers you need and use `results.length` or a custom aggregate.
202
+ - `totalCount` appears on the `PagedResult` type but DAB does not populate it in paginated queries.
203
+
204
+ ## Seed Data Scripts
205
+
206
+ Use `RayfinClient` in a Node.js script to populate data for development and testing.
207
+ Read the base URL and publishable key from your project's `.env` file — never hardcode them.
208
+
209
+ ```typescript
210
+ import { readFileSync } from 'fs';
211
+ import { RayfinClient } from '@microsoft/rayfin-client';
212
+ import type { AppSchema } from '../rayfin/data/schema';
213
+
214
+ // Read connection details from .env (generated by rayfin up)
215
+ function loadEnv(): Record<string, string> {
216
+ const vars: Record<string, string> = {};
217
+ for (const line of readFileSync('.env', 'utf-8').split('\n')) {
218
+ const match = line.match(/^([^#=]+)=(.+)$/);
219
+ if (match) vars[match[1].trim()] = match[2].trim();
220
+ }
221
+ return vars;
222
+ }
223
+
224
+ const env = loadEnv();
225
+ const client = new RayfinClient<AppSchema>({
226
+ baseUrl: env['VITE_RAYFIN_API_URL'],
227
+ publishableKey: env['VITE_RAYFIN_PUBLISHABLE_KEY'],
228
+ authStorage: false, // Required — Node.js has no localStorage
229
+ });
230
+
231
+ async function seed() {
232
+ // Sign up and sign in (email/password works for local dev only)
233
+ try {
234
+ await client.auth.signUp({ email: 'admin@example.com', password: 'Admin123!' });
235
+ } catch { /* user may already exist */ }
236
+ await client.auth.signIn({ email: 'admin@example.com', password: 'Admin123!' });
237
+
238
+ // Create entities — parent records first, then children referencing their IDs
239
+ const notebook = await client.data.Notebook.create({
240
+ name: 'Work',
241
+ isDefault: true,
242
+ createdAt: new Date(),
243
+ updatedAt: new Date(),
244
+ });
245
+
246
+ await client.data.Note.create({
247
+ title: 'First note',
248
+ content: 'Hello world',
249
+ isPinned: false,
250
+ isArchived: false,
251
+ createdAt: new Date(),
252
+ updatedAt: new Date(),
253
+ notebook: { id: notebook.id },
254
+ });
255
+ }
256
+
257
+ seed().catch(console.error);
258
+ ```
259
+
260
+ Run with `npx tsx scripts/seed.ts`, or add `"seed": "npx tsx scripts/seed.ts"` to `package.json`.
261
+
262
+ Key rules:
263
+
264
+ - Set `authStorage: false` — Node.js has no `localStorage` and the client will crash without it.
265
+ - Read `baseUrl` and `publishableKey` from `.env`, not hardcoded values — the port and key vary per project.
266
+ - Email/password auth works for **local development seeding only**.
267
+ Deployed Fabric apps use Entra SSO exclusively.
@@ -0,0 +1,20 @@
1
+ ---
2
+ sidebar_position: 30
3
+ ---
4
+
5
+ # Data
6
+
7
+ Define entities with decorators and Rayfin generates DAB-compliant schema, REST, and GraphQL endpoints.
8
+ To get started
9
+
10
+ - [Define Data Models](./overview.md)
11
+ - [Data client for CRUD operations](./graphql.md)
12
+ - [Data permissions](./permissions.md)
13
+ - [Form validation](./validation.md)
14
+
15
+ ## Known Limitations
16
+
17
+ - Date comparison queries (gt, lt, gte, lte) do not work when querying a Postgres database: [DAB issue #3094](https://github.com/Azure/data-api-builder/issues/3094)
18
+ - Querying for the total count is unsupported: [DAB discussion #2234](https://github.com/Azure/data-api-builder/discussions/2234), [DAB issue #2369](https://github.com/Azure/data-api-builder/issues/2369)
19
+ - Paginated querying with before is unsupported: [DAB issue #2238](https://github.com/Azure/data-api-builder/issues/2238)
20
+ - Nested entity querying beyond the second level is currently not supported because `GraphQLQueryBuilder.buildFieldSelection()` only supports 2-level nesting (uses `split('.', 2)`).
@@ -0,0 +1,270 @@
1
+ ---
2
+ sidebar_position: 1
3
+ ---
4
+
5
+ # Configure Rayfin Data Models
6
+
7
+ In this guide, you will learn how to set up your data schema and define your data models.
8
+ Each data model is defined with `@entity()` decorator.
9
+ You can secure your API with authorization rules using `@role()` (or the `@anonymous()` and `@authenticated()` shorthands).
10
+
11
+ ## Tables as Entity
12
+
13
+ Add `@entity()` decorator to define your model.
14
+ Import the types you need from `rayfin-core`.
15
+
16
+ **Example**
17
+
18
+ ```typescript
19
+ import { entity, authenticated, uuid, text, date, many, boolean } from '@microsoft/rayfin-core';
20
+
21
+ @entity()
22
+ @authenticated('*', {
23
+ policy: (claims, item) => claims.sub.eq(item.user_id),
24
+ })
25
+ export class Notebook {
26
+ @uuid() id!: string;
27
+ @text() name!: string;
28
+ @text({ optional: true }) description?: string;
29
+ @text({ optional: true }) color?: string;
30
+ @boolean() isDefault!: boolean;
31
+ @date() createdAt!: Date;
32
+ @date() updatedAt!: Date;
33
+ @many(() => Note) notes?: Note[];
34
+ @text() user_id!: string;
35
+ }
36
+ ```
37
+
38
+ - Every entity has an `id` field typed as `string` that serves as the UUID primary key.
39
+ If you do not declare `id` explicitly, Rayfin adds it to your schema automatically.
40
+ The `id` is optional when creating items — the server generates one if omitted.
41
+ - Policies can reference identifiers in JWT claims via the typed DSL.
42
+ Only `sub`, `email`, and `role` are supported claims.
43
+
44
+ ## Entity primary key
45
+
46
+ Every Rayfin entity uses a UUID `string` field named `id` as its primary key.
47
+ If you do not declare `id` in your entity class, Rayfin adds it to the schema automatically.
48
+ You do not need to mark the field with any special option.
49
+
50
+ - Optionally declare `id` as `@uuid() id!: string;` if you want it visible in your TypeScript class.
51
+ - The `id` field is **optional during create operations**.
52
+ If you omit it, the server generates a UUID automatically.
53
+ - You may supply your own UUID at creation time if you prefer client-generated identifiers.
54
+ - Composite or non-`id` primary keys are not supported.
55
+
56
+ ```typescript
57
+ @entity()
58
+ export class Todo {
59
+ @uuid() id!: string; // UUID primary key, auto-generated when omitted
60
+ @text() title!: string;
61
+ }
62
+ ```
63
+
64
+ ## Data types supported
65
+
66
+ Developer should be able to define the types as shown below that will allow us to define the schema when applying to the database.
67
+
68
+ | Decorator | Logical type | Notes |
69
+ | --- | --- | --- |
70
+ | `@uuid()` | UUID | Unique identifier. |
71
+ | `@text()` | string | String fields. On MSSQL, omitting `max` produces `NVARCHAR(MAX)` columns which can cause GraphQL schema generation failures at deploy time. Always specify `max` — e.g., `@text({ max: 200 })`. |
72
+ | `@int()` | int | Integer type. |
73
+ | `@decimal()` | decimal | Decimal or numeric type, depending on dialect. |
74
+ | `@boolean()` | boolean | True or false type. |
75
+ | `@date()` | datetime | Serializes from ISO strings or `Date` objects. |
76
+ | `@email()` | string | Text field with email validation. |
77
+ | `@set()` | enum | Enumerated set of string literals. |
78
+
79
+ ## Type modifiers
80
+
81
+ You can add modifiers to the your fields based on the type of the field.
82
+
83
+ - `{optional: boolean}` — make the field nullable in the database.
84
+ Fields are required (non-nullable) by default; use `{ optional: true }` to explicitly allow NULL values.
85
+ - `{unique: boolean}` — add a unique constraint for this field.
86
+ - `{default: value}` — default value expression for the column.
87
+ - `{max: n}`, `{min: n}` — string length constraints (also used for numeric range constraints).
88
+ - `{pattern: "regex"}` — string validation pattern.
89
+
90
+ ## Relationships and Ownership
91
+
92
+ One to many and many to one are supported with Rayfin.
93
+ Use `@one(() => Parent)` and `@many(() => Child)` to describe navigation properties without writing SQL joins.
94
+ Many to many currently is not supported.
95
+
96
+ Rayfin auto-generates relationship columns when you define navigation decorators.
97
+ Define foreign key fields only if you need to read or set them in application code.
98
+ When you do define them, use the `{property}_id` naming convention.
99
+
100
+ **Example**
101
+
102
+ ```typescript
103
+ import { entity, authenticated, text, set, date, uuid, boolean, one } from '@microsoft/rayfin-core';
104
+ import { Notebook } from './Notebook.js';
105
+
106
+ @entity()
107
+ @authenticated('*', {
108
+ policy: (claims, item) => claims.sub.eq(item.user_id),
109
+ })
110
+ export class Note {
111
+ @uuid() id!: string;
112
+ @text() title!: string;
113
+ @text() content!: string;
114
+ @set('markdown', 'html', 'plaintext')
115
+ contentType!: 'markdown' | 'html' | 'plaintext';
116
+ @boolean() isPinned!: boolean;
117
+ @boolean() isArchived!: boolean;
118
+ @date() createdAt!: Date;
119
+ @date() updatedAt!: Date;
120
+ @uuid() notebook_id!: string;
121
+ @one(() => Notebook, { optional: true }) notebook?: Notebook;
122
+ @text() user_id!: string;
123
+ }
124
+ ```
125
+
126
+ When authoring entity files, use `.js` extensions for relative imports so the emitted ESM JavaScript resolves correctly.
127
+
128
+ ## Using relationships in CRUD operations
129
+
130
+ After defining `@one()` and `@many()` relationships, you can create or update related entities by passing either a full object or an ID-only object.
131
+ The ID-only shape is the recommended shorthand for most write operations.
132
+
133
+ ```typescript
134
+ await client.data.Note.create({
135
+ title: 'Draft architecture review',
136
+ content: 'Outline relationship behavior',
137
+ notebook: { id: 'notebook-123' },
138
+ });
139
+
140
+ await client.data.Note.update(
141
+ { id: 'note-456' },
142
+ {
143
+ notebook: { id: 'notebook-789' },
144
+ }
145
+ );
146
+ ```
147
+
148
+ You can still pass a full related object when you already have it available.
149
+ Both forms are supported and produce equivalent relationship updates.
150
+
151
+ ```typescript
152
+ await client.data.Note.create({
153
+ title: 'Weekly summary',
154
+ content: 'Use full object when convenient',
155
+ notebook: {
156
+ id: 'notebook-123',
157
+ name: 'Work',
158
+ isDefault: false,
159
+ createdAt: new Date(),
160
+ updatedAt: new Date(),
161
+ user_id: 'user-1',
162
+ },
163
+ });
164
+ ```
165
+
166
+ For complete mutation examples and response behavior, see [Use GraphQL Query Builder](./graphql.md).
167
+
168
+ ## Schema registration
169
+
170
+ `schema.ts` binds entity names to their classes so `RayfinClient` can provide GraphQL proxies.
171
+
172
+ ```typescript
173
+ import type { Note } from './Note.js';
174
+ import type { Notebook } from './Notebook.js';
175
+
176
+ export type NotesAppSchema = {
177
+ Note: Note;
178
+ Notebook: Notebook;
179
+ };
180
+ ```
181
+
182
+ Add every new entity to this map and update the exported type wherever it is imported (most commonly in service factories).
183
+
184
+ ## Applying Model Changes
185
+
186
+ Ensure the correct backend is running and ports are free before applying.
187
+ If you switch templates or database dialects, stop or purge stale services to avoid hitting the wrong endpoint.
188
+
189
+ 1. Redeploy with `npx rayfin up` and wait for services to be healthy.
190
+ 2. Regenerate the schema by running `npx rayfin up db apply`.
191
+ 3. Refresh the frontend client.
192
+
193
+ The regeneration step updates tables, relationships, and permissions so subsequent API calls reflect your latest decorators.
194
+
195
+ ## Authorization rules using `@role()`
196
+
197
+ Rayfin uses class-level role decorators to generate Data API Builder (DAB) permissions.
198
+ Use `@role()` directly, or the `@anonymous()` and `@authenticated()` shorthands.
199
+
200
+ > **Anonymous data access is not currently supported on Fabric.**
201
+ > Both `@anonymous(...)` and `@role('anonymous', …)` are accepted at compile time, but `rayfin dev` and `rayfin up`, along with any sub-command that involves creating, updating, or applying data schema, reject any DAB configuration that grants the `anonymous` role. Remove the `@anonymous(...)` decorator (or `@role('anonymous', …)`) from your entities; all access then goes through the `authenticated` role.
202
+ >
203
+ > The `@anonymous()` shorthand is published from the experimental subpath as a preview of the future supported syntax:
204
+ >
205
+ > ```typescript
206
+ > import { anonymous } from '@microsoft/rayfin-core/experimental';
207
+ > ```
208
+ >
209
+ > The `@role('anonymous', …)` form is also importable from `@microsoft/rayfin-core/experimental` and behaves identically.
210
+
211
+ ### Public read, authenticated write
212
+
213
+ The shape below previews the future syntax for public-read access. Anonymous data access is **not currently supported on Fabric**, so applying this schema fails today; remove `@anonymous('read')` to fall back to authenticated-only access.
214
+
215
+ ```typescript
216
+ import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core';
217
+ import { anonymous } from '@microsoft/rayfin-core/experimental';
218
+
219
+ @entity()
220
+ @anonymous('read')
221
+ @authenticated(['create', 'read', 'update', 'delete'], {
222
+ policy: (claims, item) => claims.sub.eq(item.user_id),
223
+ })
224
+ export class Todo {
225
+ @uuid() id!: string;
226
+ @text() title!: string;
227
+ @text() user_id!: string;
228
+ }
229
+ ```
230
+
231
+ ### Field visibility
232
+
233
+ Use `include` or `exclude` in the role options to control which fields are visible for that role.
234
+
235
+ ```typescript
236
+ import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core';
237
+
238
+ @entity()
239
+ @authenticated('read', {
240
+ policy: (claims, item) => claims.sub.eq(item.owner_id),
241
+ exclude: ['secret'],
242
+ })
243
+ export class Document {
244
+ @uuid() id!: string;
245
+ @text() owner_id!: string;
246
+ @text() title!: string;
247
+ @text({ optional: true }) secret?: string;
248
+ }
249
+ ```
250
+
251
+ ### Notes
252
+
253
+ - `@role()` applies to classes.
254
+ Field visibility is configured through role options.
255
+ - Only the built-in roles are supported today: `anonymous` and `authenticated`.
256
+
257
+ ## Best practices
258
+
259
+ - Include a `user_id` field when using per-user policies.
260
+ - Start with restrictive permissions and expand as needed.
261
+ - Use separate `@role()` entries when field visibility differs by action.
262
+ - Prefer `@anonymous()` and `@authenticated()` shorthands for built-in roles.
263
+
264
+ ## Troubleshooting Checklist
265
+
266
+ - Missing relationships usually mean the navigation decorator is absent or the schema was not applied; foreign keys are generated automatically unless you define them explicitly.
267
+ - Authorization failures often trace back to mismatched claim names, so log the decoded JWT when debugging policies.
268
+ - If the frontend still returns stale shapes, delete `rayfin/.temp/` within the sample and rerun `npx rayfin up` to force regeneration.
269
+
270
+ Use these patterns as building blocks for any Rayfin-powered application and iteratively refine the model as product requirements evolve.