@microsoft/rayfin-guide 1.36.0-alpha.1581 → 1.36.0-alpha.1588
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.
- package/assets/docs/experimental/cli/connectors/add.md +47 -3
- package/assets/docs/experimental/cli/connectors/category-a-entities.md +15 -667
- package/assets/docs/experimental/cli/connectors/category-b-function-bridge.md +1 -1
- package/assets/docs/experimental/cli/connectors/index.md +1 -1
- package/assets/docs/getting-started/project-structure.md +0 -4
- package/assets/docs/hosting/index.md +0 -30
- package/package.json +1 -1
|
@@ -91,12 +91,56 @@ With `--json`, the same information is available under `install`:
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## Re-adding an existing connector
|
|
95
|
+
|
|
96
|
+
Running `connector add` against a name already in `rayfin.yml` refreshes the declaration.
|
|
97
|
+
It prompts before overwriting, or requires `--yes` when non-interactive.
|
|
98
|
+
|
|
99
|
+
Your generated entity files under `rayfin/connectors/<name>/` are **preserved**.
|
|
100
|
+
Only `metadata.json` is rewritten, by schema discovery.
|
|
101
|
+
The hand-authored aggregate `schema.ts` is preserved too, unless you pass `--yes` — which opts in to overwriting it with a fresh placeholder.
|
|
102
|
+
|
|
103
|
+
To start from a clean directory instead, use [`connector remove`](#related-commands) first.
|
|
104
|
+
|
|
105
|
+
## When schema discovery fails
|
|
106
|
+
|
|
107
|
+
Schema discovery is best-effort.
|
|
108
|
+
If it fails, the connector is still written to `rayfin.yml` and the command still exits `0` — you can retry discovery later by re-running `connector add`.
|
|
109
|
+
|
|
110
|
+
The failure is reported as a warning with a recovery hint, and under `--json` as a `schemaDiscovery` object:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"status": "success",
|
|
115
|
+
"action": "connector.add",
|
|
116
|
+
"schemaDiscovery": {
|
|
117
|
+
"status": "failed",
|
|
118
|
+
"reason": "login",
|
|
119
|
+
"error": "The data source rejected the login for connector \"inventory\". ...",
|
|
120
|
+
"recovery": "Confirm your account has read access to item <id> in workspace <id> — that is the usual cause. If it does, run `rayfin login` to refresh your session and retry."
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`schemaDiscovery` is present only for Category A connectors, which are the only ones that run discovery.
|
|
126
|
+
Top-level `status` stays `success` because the connector was added; check `schemaDiscovery.status` to branch on discovery specifically.
|
|
127
|
+
|
|
128
|
+
| `reason` | Meaning |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `permission` | The data source explicitly denied the read. Someone must grant your account access. |
|
|
131
|
+
| `login` | The login was rejected without saying why. Usually missing access to the item; occasionally a stale CLI session. |
|
|
132
|
+
| `auth` | The control plane rejected your token. Run `rayfin login`. |
|
|
133
|
+
| `unknown` | Anything else — the raw driver message is in `error`. |
|
|
134
|
+
|
|
94
135
|
## Related commands
|
|
95
136
|
|
|
96
137
|
```bash
|
|
97
|
-
npx rayfin connector list
|
|
138
|
+
npx rayfin connector list --json
|
|
139
|
+
npx rayfin connector list --verbose
|
|
98
140
|
|
|
99
|
-
# Removes the rayfin.yml entry AND the rayfin/connectors/<name>/ directory
|
|
100
|
-
#
|
|
141
|
+
# Removes the rayfin.yml entry AND the rayfin/connectors/<name>/ directory,
|
|
142
|
+
# including any entity files you generated.
|
|
101
143
|
npx rayfin connector remove <name> [--yes]
|
|
102
144
|
```
|
|
145
|
+
|
|
146
|
+
`--json` and `--verbose` are mutually exclusive and cannot be combined.
|
|
@@ -4,670 +4,18 @@ sidebar_position: 8
|
|
|
4
4
|
|
|
5
5
|
# Category A — GraphQL entity connectors
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
1. Read `rayfin/connectors/<name>/metadata.json` (shape: `SchemaMetadata` — see the [metadata.json reference](#metadatajson-reference)).
|
|
24
|
-
2. Pick the tables in scope.
|
|
25
|
-
- **Full set** — every table under `schemas[].tables[]`.
|
|
26
|
-
- **Subset** — only the tables the user named. Filter `schemas[].tables[]` by `tableName` before generating.
|
|
27
|
-
3. For each surviving table, write a file at `rayfin/connectors/<name>/<EntityName>.ts` following the [Entity generation contract](#entity-generation-contract) exactly.
|
|
28
|
-
4. Overwrite the placeholder `rayfin/connectors/<name>/schema.ts` with the [aggregate schema](#aggregate-connector-schema).
|
|
29
|
-
5. Surface every warning the contract emits (no-FK-metadata notes, unknown SQL types). Do not drop them silently.
|
|
30
|
-
|
|
31
|
-
When the user asks for a refresh ("regenerate everything"), re-do the same flow against the existing `metadata.json`.
|
|
32
|
-
Only re-run `rayfin connector remove` and `rayfin connector add` when the *source schema* changed and you need fresh metadata.
|
|
33
|
-
|
|
34
|
-
## Scope `@role(...)` on entities
|
|
35
|
-
|
|
36
|
-
The YAML `operations:` controls which actions reach the source.
|
|
37
|
-
The entity-level `@role(...)` decorator controls which roles can perform which actions on that entity.
|
|
38
|
-
|
|
39
|
-
The entity's `actions` list **must be a subset** of the YAML `operations:` for that connector.
|
|
40
|
-
The host's settings validator does not catch the mismatch today — DAB will fail at `rayfin up connector apply` time.
|
|
41
|
-
Always narrow YAML first, then add `@role(...)` to entities.
|
|
42
|
-
|
|
43
|
-
Example: a `fabric-warehouse` connector with `operations: [read, update]` and an `Order` entity locked to read-only:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
import { entity, int, text, decimal, Source, role } from '@microsoft/rayfin-core';
|
|
47
|
-
|
|
48
|
-
@role('authenticated', ['read'])
|
|
49
|
-
@entity()
|
|
50
|
-
export class Order extends Source({ schema: 'dbo', table: 'Order', primaryKey: ['orderId'] }) {
|
|
51
|
-
@int({ column: 'OrderID' }) orderId!: number;
|
|
52
|
-
@text() customerEmail!: string;
|
|
53
|
-
@decimal({ precision: 18, scale: 2 }) total!: number;
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Legal `actions`: `'read' | 'create' | 'update' | 'delete' | '*'`.
|
|
58
|
-
Stack multiple `@role(...)` decorators to give different roles different actions on the same entity.
|
|
59
|
-
|
|
60
|
-
## Row-level policies
|
|
61
|
-
|
|
62
|
-
Use a `policy` callback on `@role(...)` for row-level security.
|
|
63
|
-
Policies use the typed `claims` / `item` DSL — never raw SQL strings.
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
@role('authenticated', ['read', 'update'], {
|
|
67
|
-
policy: (claims, item) => claims.sub.eq(item.owner_id),
|
|
68
|
-
})
|
|
69
|
-
@entity()
|
|
70
|
-
export class Todo extends Source({ schema: 'dbo', table: 'Todo', primaryKey: ['id'] }) {
|
|
71
|
-
@uuid() id!: string;
|
|
72
|
-
@uuid() owner_id!: string;
|
|
73
|
-
@text() body?: string;
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
DSL surface: `claims.sub | email | role`, `item.<columnName>`, `.eq(...)`, `.and(...)`, `.or(...)`.
|
|
78
|
-
Use any claim the same way — for example `claims.email.eq(item.user_email)`.
|
|
79
|
-
`RoleDeclarationOptions` also accepts `include` and `exclude` arrays for field-level allow/block lists.
|
|
80
|
-
|
|
81
|
-
When to prompt the user: inspect `metadata.json` for columns named `owner_id`, `user_id`, `tenant_id`, `created_by`.
|
|
82
|
-
If you see one, ask whether that column should restrict rows so each authenticated user only sees their own.
|
|
83
|
-
|
|
84
|
-
## Aggregate connector schema
|
|
85
|
-
|
|
86
|
-
Install the connector packages first.
|
|
87
|
-
`rayfin connector add` scaffolds files but installs nothing, and the aggregate `schema.ts` imports two packages a fresh app does not yet declare.
|
|
88
|
-
|
|
89
|
-
`connector add` prints the exact command to run — copy it from that output, or rebuild it from the `packages` array in `rayfin connector types --json`, which carries both the package names and the version:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
# Shape only. Use the version connector add printed, not this one.
|
|
93
|
-
npm install @microsoft/rayfin-connector-fabric-graphql@1.35.0-alpha @microsoft/rayfin-connectors@1.35.0-alpha
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
**Always pin the version.** Connector packages ship in lockstep with the CLI, but their npm `latest` and `preview` tags lag behind.
|
|
97
|
-
An unversioned `npm install` resolves to an older release that hard-pins its own `@microsoft/rayfin-data`, leaving two Rayfin version lines in one app.
|
|
98
|
-
|
|
99
|
-
- `@microsoft/rayfin-connector-fabric-graphql` — provides `GraphQLBackedConnector`. Not a dependency of `@microsoft/rayfin-client`, so it is always missing on a fresh app.
|
|
100
|
-
- `@microsoft/rayfin-connectors` — provides `ConnectorConfig`. It ships transitively under `@microsoft/rayfin-client`, but declare it directly so strict resolvers (pnpm) do not treat it as a phantom dependency.
|
|
101
|
-
|
|
102
|
-
Even though both imports are `import type`, TypeScript still needs the packages present at compile time.
|
|
103
|
-
|
|
104
|
-
The aggregate `rayfin/connectors/<name>/schema.ts` is what the app imports from.
|
|
105
|
-
The CLI leaves a placeholder there; overwrite it so it exports **three** things:
|
|
106
|
-
|
|
107
|
-
1. **Entity re-exports** — re-export every entity class you generated for this connector, **as types** (`export type { ... }`).
|
|
108
|
-
2. **`<Name>Schema` (type)** — a `GraphQLBackedConnector<TSchema, typeof connectorConfig>` marker.
|
|
109
|
-
`TSchema` maps each in-scope entity name to its class via `typeof`; the second argument is `typeof connectorConfig`, from which the marker derives both the CRUD operation union and the dialect.
|
|
110
|
-
This is what makes `client.connectors.<name>.<Entity>.select(...)` strongly typed and gates CRUD methods to the declared operations at compile time.
|
|
111
|
-
3. **`connectorConfig` (value)** — a `ConnectorConfig` carrying the runtime `connector` type, the same `operations` allow-list, and an **`entities` map** keyed exactly as in `TSchema` and covering every entity there.
|
|
112
|
-
The runtime routes on `connector`, gates CRUD verbs by `operations`, and uses `entities` as the default selection so a read or write without an explicit `select` returns the full row.
|
|
113
|
-
Give the entries as **string arrays** of entity property names, not entity classes — see below.
|
|
114
|
-
Only the class form carries relationship cardinality, so a relationship `select` against the array form throws `ENTITIES_REQUIRED_FOR_RELATIONSHIP_SELECT`.
|
|
115
|
-
|
|
116
|
-
`GraphQLBackedConnector<TSchema, typeof connectorConfig>` **is** the published typed marker for Category A connectors.
|
|
117
|
-
Never invent per-type names like `FabricWarehouse` or `FabricSqlAnalytics` — they do not exist.
|
|
118
|
-
|
|
119
|
-
### Import entities as types, never as values
|
|
120
|
-
|
|
121
|
-
This file is imported by browser code, because the app client reads `connectorConfig` from it.
|
|
122
|
-
A value import pulls the decorated entity classes into the browser bundle with it.
|
|
123
|
-
|
|
124
|
-
That breaks the app. `vite build` still exits 0, type-check passes, lint passes, and deploy succeeds, but the bundler lowers the decorators into an invalid class expression (`var Order = @Source({...})`), the emitted bundle fails to parse, and the page renders blank with `Uncaught SyntaxError: Invalid or unexpected token`.
|
|
125
|
-
Entity classes are build-time declarations consumed by DAB config generation. They have no business in a browser bundle.
|
|
126
|
-
|
|
127
|
-
Every entity reference in this file is a `typeof` lookup, which is pure type information, so `import type` and `export type` are sufficient:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
import type { Order } from './Order.js'; // correct
|
|
131
|
-
import { Order } from './Order.js'; // wrong — ships the decorated class
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
The same applies to the re-exports. Use `export type { Order }`, not `export { Order }`.
|
|
135
|
-
|
|
136
|
-
This is also why `entities` takes column names rather than classes.
|
|
137
|
-
|
|
138
|
-
### Single source of truth for operations
|
|
139
|
-
|
|
140
|
-
For one connector, the `connectorConfig.operations` array and the YAML `operations:` must describe the same verb set — that pair is the connector-wide ceiling.
|
|
141
|
-
Each entity's `@role(...)` actions stay a **subset** of it, so a read-only entity keeps `['read']` on a read/update connector.
|
|
142
|
-
Narrow YAML first, mirror it into `connectorConfig.operations`, then grant each entity only the verbs it actually needs.
|
|
143
|
-
Never widen a decorator to match the connector's full verb set.
|
|
144
|
-
|
|
145
|
-
### Subset rule
|
|
146
|
-
|
|
147
|
-
In subset mode, include in `TSchema` only the entities you actually generated.
|
|
148
|
-
Never reference an entity class you did not generate — the `typeof` lookup and its import would dangle.
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
// rayfin/connectors/inventory/schema.ts
|
|
152
|
-
import type { GraphQLBackedConnector } from '@microsoft/rayfin-connector-fabric-graphql';
|
|
153
|
-
import type { ConnectorConfig } from '@microsoft/rayfin-connectors';
|
|
154
|
-
|
|
155
|
-
import type { Order } from './Order.js';
|
|
156
|
-
import type { Customer } from './Customer.js';
|
|
157
|
-
|
|
158
|
-
export type { Order } from './Order.js';
|
|
159
|
-
export type { Customer } from './Customer.js';
|
|
160
|
-
|
|
161
|
-
// Use `as const satisfies` (not a `: ConnectorConfig` annotation) so the
|
|
162
|
-
// `connector` and `operations` literals survive — the marker reads them to
|
|
163
|
-
// derive the permitted operations and the dialect.
|
|
164
|
-
export const connectorConfig = {
|
|
165
|
-
connector: 'fabric-warehouse',
|
|
166
|
-
operations: ['read', 'update'],
|
|
167
|
-
entities: {
|
|
168
|
-
Order: ['orderId', 'customerId', 'total', 'placedUtc'],
|
|
169
|
-
Customer: ['customerId', 'email'],
|
|
170
|
-
},
|
|
171
|
-
} as const satisfies ConnectorConfig;
|
|
172
|
-
|
|
173
|
-
export type InventorySchema = GraphQLBackedConnector<
|
|
174
|
-
{ Order: typeof Order; Customer: typeof Customer },
|
|
175
|
-
typeof connectorConfig
|
|
176
|
-
>;
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
The pattern scales to any number of entities under any names — add one `import type` line, one `export type` line, and one `TSchema` key per entity.
|
|
180
|
-
|
|
181
|
-
The `<Name>Schema` name is the PascalCase connector name plus `Schema` (connector `inventory` → `InventorySchema`).
|
|
182
|
-
For a read-only Lakehouse (`fabric-sqlanalytics`), `operations` is `['read']`.
|
|
183
|
-
|
|
184
|
-
### The `entities` map
|
|
185
|
-
|
|
186
|
-
`connectorConfig.entities` gives the runtime each entity's field names, and it is what makes a call without an explicit `select` work:
|
|
187
|
-
|
|
188
|
-
- Reads — `findMany(filter?)`, `findFirst(filter?)` and `findByKey` use it as the default selection to return the full row.
|
|
189
|
-
- Writes on `fabric-sqldatabase` — `create` / `update` / `delete` use it to read the row back, including server-generated columns the caller never sent.
|
|
190
|
-
|
|
191
|
-
**Omitting it is not optional.** Without it, every no-selection read throws `SELECTION_REQUIRED`, on every dialect.
|
|
192
|
-
|
|
193
|
-
Give the names as **string arrays**, and list every entity in `TSchema`.
|
|
194
|
-
The map also accepts entity classes, but do not use that form here: it puts the decorated classes back in the browser bundle and reintroduces the blank page above.
|
|
195
|
-
|
|
196
|
-
**Use the entity's property names, not the database column names.**
|
|
197
|
-
These are the names on the generated class — the same keys that appear in the row type — not the `columnName` values in `metadata.json`.
|
|
198
|
-
For `@uuid({ column: 'ProductID' }) productId!: string`, the entry is `productId`.
|
|
199
|
-
Include every scalar field and leave relationship fields out, since those need nested sub-selections.
|
|
200
|
-
|
|
201
|
-
**Relationship selects need the class form.**
|
|
202
|
-
Cardinality (`@one` versus `@many`) lives in the decorator metadata, which a name array cannot carry, so selecting a relationship path against the array form throws `ENTITIES_REQUIRED_FOR_RELATIONSHIP_SELECT`.
|
|
203
|
-
Scalar reads and writes are unaffected.
|
|
204
|
-
If an app needs relationship selects in browser code, select the foreign key scalar and fetch the related entity separately, or move that query server-side where the class can be imported.
|
|
205
|
-
|
|
206
|
-
## Wire connectors into the app client
|
|
207
|
-
|
|
208
|
-
Declaring and deploying a connector does not make it callable from app code.
|
|
209
|
-
Expose it through a `ConnectorsRayfinClient` so it shows up as `client.connectors.<name>`.
|
|
210
|
-
|
|
211
|
-
`ConnectorsRayfinClient` is experimental — import it only from the `@microsoft/rayfin-client/experimental` subpath, never the stable `@microsoft/rayfin-client` entry.
|
|
212
|
-
|
|
213
|
-
The connector key must be identical in three places: the `name` in `rayfin.yml`, the property in `AppConnectorsSchema`, and the property in the `connectors` option.
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental';
|
|
217
|
-
import { ProductDbSchema, connectorConfig as productDbConfig } from '../../rayfin/connectors/productDb/schema';
|
|
218
|
-
import { InventorySchema, connectorConfig as inventoryConfig } from '../../rayfin/connectors/inventory/schema';
|
|
219
|
-
|
|
220
|
-
type AppConnectorsSchema = {
|
|
221
|
-
productDb: ProductDbSchema;
|
|
222
|
-
inventory: InventorySchema;
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
export function getRayfinClient() {
|
|
226
|
-
// Type params are <DataSchema, FunctionsSchema, ConnectorsSchema> — set the
|
|
227
|
-
// unused slots to Record<string, never>, or pass real schema types if the app
|
|
228
|
-
// also has a data model / functions.
|
|
229
|
-
return new ConnectorsRayfinClient<Record<string, never>, Record<string, never>, AppConnectorsSchema>({
|
|
230
|
-
baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
|
|
231
|
-
publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
|
|
232
|
-
authStorage: true,
|
|
233
|
-
connectors: {
|
|
234
|
-
productDb: productDbConfig,
|
|
235
|
-
inventory: inventoryConfig,
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Verify by type-checking the project (`tsc --noEmit` or the app build).
|
|
242
|
-
A mismatch between a connector key in `AppConnectorsSchema` and the `connectors` option surfaces there.
|
|
243
|
-
|
|
244
|
-
## Reading and writing connector entities
|
|
245
|
-
|
|
246
|
-
Each entity is reached through `client.connectors.<name>.<Entity>`.
|
|
247
|
-
What the surface exposes depends on the connector's `operations:` (which gate the verbs) and the connector type/dialect (which shapes what writes return).
|
|
248
|
-
|
|
249
|
-
### Reads
|
|
250
|
-
|
|
251
|
-
Reads work identically on Lakehouse, Warehouse, and SQL Database — only writes differ.
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
// Query chain: select -> where -> orderBy -> execute
|
|
255
|
-
const orders = await client.connectors.inventory.Order
|
|
256
|
-
.select(['orderId', 'customerEmail', 'total'])
|
|
257
|
-
.where({ total: { gt: 100 } })
|
|
258
|
-
.orderBy({ total: 'desc' })
|
|
259
|
-
.execute();
|
|
260
|
-
|
|
261
|
-
// By-key read. Pass a key OBJECT (all parts of the composite PK) AND a
|
|
262
|
-
// required scalar-only `select`. The result is Pick<Row, selected> | null.
|
|
263
|
-
const order = await client.connectors.inventory.Order.findByKey(
|
|
264
|
-
{ orderId: 'o-1' },
|
|
265
|
-
['orderId', 'customerEmail', 'total'],
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const lineItem = await client.connectors.sales.OrderItem.findByKey(
|
|
269
|
-
{ orderId: 'o-1', productId: 'p-9' }, // both parts of the composite PK required
|
|
270
|
-
['orderId', 'productId', 'quantity'],
|
|
271
|
-
);
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
`findByKey` takes the key object and a **required** `select` of scalar columns, returning `Pick<Row, selected> | null`.
|
|
275
|
-
Relationships cannot be selected via `findByKey` — read them through the query chain.
|
|
276
|
-
A composite key requires all its parts in the key object; omitting one is a compile error.
|
|
277
|
-
|
|
278
|
-
### Reading related columns
|
|
279
|
-
|
|
280
|
-
`select` can pull columns from a related entity — a `@one` (forward FK) or `@many` (reverse FK) navigation field — by naming them as a **dotted path**.
|
|
281
|
-
The builder expands each path into the nested selection the server expects and unwraps the response so the related rows sit inline on the result.
|
|
282
|
-
|
|
283
|
-
```ts
|
|
284
|
-
const products = await client.connectors.inventory.Product
|
|
285
|
-
.select(['name', 'category.name', 'orderItems.quantity'])
|
|
286
|
-
.where({ stock: { gt: 0 } })
|
|
287
|
-
.execute();
|
|
288
|
-
|
|
289
|
-
products[0].category.name; // @one — related row inline
|
|
290
|
-
products[0].orderItems[0].quantity; // @many — related rows inline (unwrapped)
|
|
291
|
-
|
|
292
|
-
// Paths nest to arbitrary depth, hopping across entities.
|
|
293
|
-
await client.connectors.inventory.Product
|
|
294
|
-
.select(['name', 'orderItems.order.customerEmail'])
|
|
295
|
-
.execute();
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
- Name related columns as a dotted path (`category.name`), never the bare relationship (`category`) — a navigation field is not a selectable leaf and is a compile error.
|
|
299
|
-
- Each path is validated segment-by-segment against the schema, so a wrong hop (`category.nope`) fails to compile; depth is unbounded.
|
|
300
|
-
- `findByKey` cannot select relationships — it is scalar-only.
|
|
301
|
-
|
|
302
|
-
Self-referencing foreign keys generate an entity and a DAB relationship, but the client cannot query across one.
|
|
303
|
-
Do not select a dotted path over a self-relationship.
|
|
304
|
-
|
|
305
|
-
### Writes
|
|
306
|
-
|
|
307
|
-
`create` / `update` / `delete` exist only when the connector's `operations:` and the entity `@role(...)` allow them, and what they return is fixed by the connector's dialect:
|
|
308
|
-
|
|
309
|
-
| Connector type | Dialect | `create`/`update`/`delete` return | Notes |
|
|
310
|
-
| --- | --- | --- | --- |
|
|
311
|
-
| `fabric-sqlanalytics` (Lakehouse) | — | **not available** | Read-only at the host; only `select`/`findByKey`/query chain exist. Writes are a compile error. |
|
|
312
|
-
| `fabric-sqldatabase` (SQL Database) | row-returning | the **full entity row** | Reads the whole row back after the write — every column, including server-generated ones the caller did not send. Needs the [`entities` map](#the-entities-map). |
|
|
313
|
-
| `fabric-warehouse` (Warehouse) | no `OUTPUT` clause | `DbOperationResult { result: string }` | DWSQL cannot read the row back, so mutations resolve to a status object. On success `result` is `"success"`; a failed write throws a GraphQL error. |
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
// SQL Database — mutation returns the full row, with every server-generated
|
|
317
|
-
// column filled in by the database.
|
|
318
|
-
const created = await client.connectors.orders.Order.create({
|
|
319
|
-
quantity: 3, // supply only the columns you own…
|
|
320
|
-
unitPrice: 19.99,
|
|
321
|
-
});
|
|
322
|
-
created.id; // server-assigned identity — returned, though never sent
|
|
323
|
-
created.createdUtc; // server default (SYSUTCDATETIME()) — returned
|
|
324
|
-
created.lineTotal; // computed (quantity * unitPrice) — returned
|
|
325
|
-
|
|
326
|
-
// Warehouse — mutation returns DbOperationResult, NOT the row
|
|
327
|
-
const result = await client.connectors.inventory.Order.update(
|
|
328
|
-
{ orderId: 'o-1' }, // key object (all composite parts)
|
|
329
|
-
{ total: 250 }, // partial update
|
|
330
|
-
);
|
|
331
|
-
result.result; // "success" — status string, not the row
|
|
332
|
-
|
|
333
|
-
// Delete by key (both dialects) — full composite key required
|
|
334
|
-
await client.connectors.sales.OrderItem.delete({ orderId: 'o-1', productId: 'p-9' });
|
|
335
|
-
|
|
336
|
-
// Lakehouse — writes do not exist
|
|
337
|
-
client.connectors.analytics.Sales.create({ /* ... */ }); // compile error: read-only
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**Server-generated columns.**
|
|
341
|
-
These are the columns marked `AutoGenerated<T>` on the entity: `IDENTITY`, any `DEFAULT`, and computed (`AS (...)`) columns.
|
|
342
|
-
They are optional on `create` / `update` and come back populated in the returned row.
|
|
343
|
-
Most cannot be written: passing an `IDENTITY` or computed value is rejected by the database, and rowversion / temporal columns are server-maintained.
|
|
344
|
-
The one exception is a plain `DEFAULT` column — omit it to get the default, or pass a value to override it.
|
|
345
|
-
|
|
346
|
-
By-key `update`/`delete` take the same full key object as `findByKey`.
|
|
347
|
-
A keyless entity (`primaryKey` omitted or `[]`) exposes no `findByKey` / `update` / `delete` at all — it is read-only regardless of the connector type.
|
|
348
|
-
|
|
349
|
-
### Quick decision guide
|
|
350
|
-
|
|
351
|
-
- **Lakehouse (`fabric-sqlanalytics`)** — reads only. Model entities, `select`, `findByKey`, query chain. No `@role` write actions.
|
|
352
|
-
- **SQL Database (`fabric-sqldatabase`)** — full CRUD; mutations hand back the full row, so you can render the created/updated record directly.
|
|
353
|
-
- **Warehouse (`fabric-warehouse`)** — full CRUD; mutations hand back a `DbOperationResult`, so re-query with `findByKey`/`select` if you need the persisted values.
|
|
354
|
-
|
|
355
|
-
For aggregation, `groupBy`, or year/quarter/month time-bucketing, search the docs (`rayfin docs search`).
|
|
356
|
-
|
|
357
|
-
## Worked example — a use case on specific entities
|
|
358
|
-
|
|
359
|
-
The user already ran `rayfin connector add` for a `fabric-warehouse` connector named `sales` and says *"I just need users to read and update orders and their line items."*
|
|
360
|
-
|
|
361
|
-
- **Scope and narrow.** Operations are `read`, `update`; set `operations:` on the `sales` entry to just those.
|
|
362
|
-
- **Generate the subset.** Filter `metadata.json` to `Order` and `OrderItem`, write both per the contract. Drop any `@one(() => Customer, ...)` to an out-of-scope table (no dangling import).
|
|
363
|
-
- **Roles and aggregate.** `@role('authenticated', ['read', 'update'])` on both; aggregate `SalesSchema = GraphQLBackedConnector<{ Order: typeof Order; OrderItem: typeof OrderItem }, typeof connectorConfig>`, with both entities imported and re-exported as **types**, and `connectorConfig` declared `as const satisfies ConnectorConfig` with `operations: ['read', 'update']`.
|
|
364
|
-
- **Wire and verify.** Add `sales` to `AppConnectorsSchema` and the `connectors` option. Type-check: `client.connectors.sales.Order.create(...)` is now a compile error — the proof the scope took effect. Deploy with `rayfin up`.
|
|
365
|
-
|
|
366
|
-
## Entity generation contract
|
|
367
|
-
|
|
368
|
-
When you generate `rayfin/connectors/<name>/<EntityName>.ts` from `metadata.json`, follow these rules end-to-end.
|
|
369
|
-
`metadata.json` is the only source of truth for physical keys and relationships — never infer either from column names, values, naming conventions, or table shape.
|
|
370
|
-
The CLI itself does not emit entity files.
|
|
371
|
-
|
|
372
|
-
### 1. File name and class name
|
|
373
|
-
|
|
374
|
-
- `className = pascalCase(table.tableName)` (for example `product_category` → `ProductCategory`).
|
|
375
|
-
- **Pluralization is allowed, but must be idempotent — never double-pluralize.**
|
|
376
|
-
Pluralizing a singular table name is fine (`Order` → `Orders`, `Category` → `Categories`).
|
|
377
|
-
But first check whether the name is already plural: if the source table is already plural (`Orders`, `Categories`, `sales_line_items`), keep it exactly as-is — do not add another plural suffix.
|
|
378
|
-
Whatever name you settle on, the `@entity` name and the `client.connectors.<name>.<Entity>` access path must stay consistent with it.
|
|
379
|
-
- File name is `<className>.ts`. One file per table; no nesting.
|
|
380
|
-
- **Cross-connector uniqueness — disambiguate only on collision.**
|
|
381
|
-
GraphQL type names are global across every connector in the app: two connectors that each produce an entity with the same name collide at `rayfin up connector apply` time even when they point at different physical tables.
|
|
382
|
-
Before finalizing a name, scan every other `rayfin/connectors/*/` directory and the entities you have already generated for the current connector for a matching class / `@entity` name.
|
|
383
|
-
If — and only if — the base name is already taken, prefix it with the connector's source database name (PascalCased, from `metadata.json` `source`): `Product` → `SalesDbProduct`.
|
|
384
|
-
Disambiguation is required even when the colliding entities refer to the same physical source table.
|
|
385
|
-
If that database name is itself shared across the colliding connectors, fall back to the PascalCased connector name (the `rayfin.yml` `name`, which the host guarantees unique).
|
|
386
|
-
Never rename a name that does not collide, and never blanket-prefix every entity.
|
|
387
|
-
When you rename an entity, use the disambiguated name everywhere: the class name, the file name, the `@entity` name, the `TSchema` key and re-export, and the `client.connectors.<name>.<Entity>` access path.
|
|
388
|
-
Surface a one-line note to the user for each rename.
|
|
389
|
-
|
|
390
|
-
### 2. Primary keys from metadata
|
|
391
|
-
|
|
392
|
-
When `table.primaryKeyColumns` is present and non-empty, declare exactly those columns on `Source(...)`, using each column's TypeScript property name and preserving `table.primaryKeyColumns` (`ORDINAL_POSITION`) order.
|
|
393
|
-
This rule applies to single-column and composite keys.
|
|
394
|
-
Keep one property per key column and preserve each SQL column name with `column:` when it differs.
|
|
395
|
-
|
|
396
|
-
```ts
|
|
397
|
-
export class OrderItem extends Source({
|
|
398
|
-
schema: 'dbo',
|
|
399
|
-
table: 'OrderItem',
|
|
400
|
-
primaryKey: ['orderId', 'productId'],
|
|
401
|
-
}) {
|
|
402
|
-
@uuid({ column: 'OrderID' }) orderId!: string;
|
|
403
|
-
@uuid({ column: 'ProductID' }) productId!: string;
|
|
404
|
-
@int() quantity!: number;
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
Every key part is then required in the by-key methods (`findByKey`/`update`/`delete`).
|
|
409
|
-
|
|
410
|
-
When `primaryKeyColumns` is absent or empty, emit `primaryKey: []`.
|
|
411
|
-
The entity is keyless and exposes no `findByKey`/`update`/`delete` methods.
|
|
412
|
-
Do not choose a column as a synthetic key, including columns named `id`, columns that appear unique in sampled data, or the first non-nullable column.
|
|
413
|
-
|
|
414
|
-
Lakehouse SQL endpoints commonly omit PK metadata.
|
|
415
|
-
For `fabric-sqlanalytics`, this means generating a read-only, keyless entity unless `primaryKeyColumns` is actually present in `metadata.json`.
|
|
416
|
-
Never add a logical PK on the agent's own initiative; a Builder may deliberately declare one later when they know the source's logical uniqueness contract.
|
|
417
|
-
|
|
418
|
-
### 3. Key-field validation
|
|
419
|
-
|
|
420
|
-
- Resolve every `primaryKeyColumns` entry against `table.columns` by exact SQL column name before converting it to a TypeScript property name.
|
|
421
|
-
- Preserve the metadata order for composite keys.
|
|
422
|
-
- A key column must be non-nullable. If metadata names a nullable or missing column, do not guess a replacement; stop generation for that table and report the inconsistency.
|
|
423
|
-
- An empty table or a table without PK metadata is keyless.
|
|
424
|
-
|
|
425
|
-
The key's property name and datatype come from the matching column metadata; the PK is not renamed to `id`.
|
|
426
|
-
Preserve the on-disk SQL column name via the `column:` option.
|
|
427
|
-
|
|
428
|
-
### 4. SQL type to decorator mapping
|
|
429
|
-
|
|
430
|
-
Look up `column.dataType` (case-insensitive) in this table:
|
|
431
|
-
|
|
432
|
-
| SQL type family | Decorator | TS type |
|
|
433
|
-
| --- | --- | --- |
|
|
434
|
-
| `int`, `bigint`, `smallint`, `tinyint` | `@int()` | `number` |
|
|
435
|
-
| `decimal`, `numeric`, `money`, `smallmoney`, `float`, `real` | `@decimal({ precision, scale })` | `number` |
|
|
436
|
-
| `bit` | `@boolean()` | `boolean` |
|
|
437
|
-
| `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset`, `time` | `@date()` | `Date` |
|
|
438
|
-
| `uniqueidentifier` | `@uuid()` | `string` |
|
|
439
|
-
| `varchar`, `nvarchar`, `char`, `nchar`, `text`, `ntext` | `@text()` | `string` |
|
|
440
|
-
| Anything else (`geography`, `hierarchyid`, `xml`, vector) | `@text()` + warning | `string` |
|
|
441
|
-
|
|
442
|
-
For the fallback case, emit a warning: `Unknown SQL type <dataType> for <table>.<column>; falling back to @text().`
|
|
443
|
-
|
|
444
|
-
### 4a. Server-generated columns
|
|
445
|
-
|
|
446
|
-
`metadata.json` flags the columns the database fills in.
|
|
447
|
-
On each column, check for these server-generation markers:
|
|
448
|
-
|
|
449
|
-
- `identity` — an `IDENTITY(seed, increment)` key.
|
|
450
|
-
- `default` — a column `DEFAULT` (for example `newid()`, `sysutcdatetime()`, `NEXT VALUE FOR <seq>`).
|
|
451
|
-
- `computed` — a computed column (`AS (<expr>)`).
|
|
452
|
-
- `serverManaged` — a column the server maintains with no user-facing expression: `'rowversion'`, `'temporalRowStart'`, or `'temporalRowEnd'`.
|
|
453
|
-
|
|
454
|
-
If any of these is present, the column is server-generated: wrap its TS type from section 4 in `AutoGenerated<…>`.
|
|
455
|
-
The decorator and its `column:` option are unchanged; do not emit a `default:` option — the marker is type-only and the connector never writes these columns.
|
|
456
|
-
|
|
457
|
-
`AutoGenerated<T>` makes the column optional on create/update input and read back as plain `T` in the returned row.
|
|
458
|
-
Nullability is unchanged: a non-null server-generated column still uses `!:`; a nullable one uses `?:`.
|
|
459
|
-
|
|
460
|
-
Optional on input does not mean "accepts a value".
|
|
461
|
-
For most server-generated columns, passing a value is rejected by the database:
|
|
462
|
-
|
|
463
|
-
- `identity` — inserting an explicit value fails. Never send it.
|
|
464
|
-
- `computed` — a computed column cannot be written; setting it is a server error.
|
|
465
|
-
- `serverManaged` — server-maintained; writing is rejected.
|
|
466
|
-
- plain `default` — the one exception: omit it to get the default, or pass a value to override it.
|
|
467
|
-
|
|
468
|
-
```ts
|
|
469
|
-
@int({ column: 'Id' })
|
|
470
|
-
id!: AutoGenerated<number>; // IDENTITY — omit on write, server assigns
|
|
471
|
-
|
|
472
|
-
@uuid({ column: 'PublicId' })
|
|
473
|
-
publicId!: AutoGenerated<string>; // DEFAULT newid()
|
|
474
|
-
|
|
475
|
-
@decimal({ optional: true, column: 'LineTotal', precision: 28, scale: 2 })
|
|
476
|
-
lineTotal?: AutoGenerated<number>; // computed: AS ([Quantity] * [UnitPrice])
|
|
477
|
-
|
|
478
|
-
@text({ column: 'RowVer' })
|
|
479
|
-
rowVer!: AutoGenerated<string>; // rowversion (serverManaged) — read-back-only
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
### 5. Field-level decorator options
|
|
483
|
-
|
|
484
|
-
For every column, the decorator option object is built in this order (omit keys you do not need):
|
|
485
|
-
|
|
486
|
-
1. `optional: true` — if `column.isNullable` is true.
|
|
487
|
-
2. `column: '<columnName>'` — when the SQL column name differs from the TS property name. Single-quote the value; escape embedded `'`.
|
|
488
|
-
3. **Text only:** `max: <maxLength>` — when the decorator is `text` and `column.maxLength > 0`.
|
|
489
|
-
4. **Decimal only:** `precision: <p>, scale: <s>` — when the decorator is `decimal` and both are present in metadata.
|
|
490
|
-
5. **Integer:** do not emit `min`/`max` based on SQL precision; those are value bounds, not storage capacity. Plain `@int()` is correct.
|
|
491
|
-
|
|
492
|
-
If the option object is empty, emit `@text()` rather than `@text({})`.
|
|
493
|
-
|
|
494
|
-
Property name is always `camelCase(column.columnName)` — the PK is not special-cased or renamed to `id`.
|
|
495
|
-
Nullable columns use `?:`; non-nullable use `!:`.
|
|
496
|
-
|
|
497
|
-
### 5a. Server-generated columns have no decorator option
|
|
498
|
-
|
|
499
|
-
`metadata.json` carries `column.identity`, `column.default`, and `column.computed`, but no field decorator has an option to mark a column as server-generated.
|
|
500
|
-
Do not invent one.
|
|
501
|
-
|
|
502
|
-
Declare the column normally — those markers change nothing about the emitted decorator, beyond the `AutoGenerated<T>` type wrapper in section 4a.
|
|
503
|
-
What they do change is what you tell the Builder: surface a one-line note for each such column (for example, `Column '<table>.<column>' is server-generated (identity); the source will populate it, so omit it on create()`).
|
|
504
|
-
|
|
505
|
-
An identity or computed column that is also a declared key follows the normal [primary key rules](#2-primary-keys-from-metadata) unchanged — being server-generated never exempts it from key-field validation.
|
|
506
|
-
|
|
507
|
-
### 6. Global-type shadow rule
|
|
508
|
-
|
|
509
|
-
If `pascalCase(table.tableName)` equals a column's TS type (for example a table named `Date` with a `datetime2` column), the unqualified `Date` in the annotation resolves to the entity class, not the global.
|
|
510
|
-
Render the type as `globalThis.Date` in that one field annotation only.
|
|
511
|
-
|
|
512
|
-
### 7. Relationships
|
|
513
|
-
|
|
514
|
-
Forward (`@one`) and reverse (`@many`) relationships are emitted from foreign-key metadata **only**.
|
|
515
|
-
Do not infer Lakehouse relationships from matching column names, star-schema naming, or sampled values.
|
|
516
|
-
|
|
517
|
-
Schema discovery records an FK on the referencing table only, so `table.foreignKeys` governs **forward** relationships alone.
|
|
518
|
-
When it is absent or empty, emit no `@one` — but still emit every `@many` the reverse-FK index below produces, because a parent table legitimately has no FKs of its own while being the target of another table's.
|
|
519
|
-
A table with neither its own FKs nor any incoming constraint gets no relationship decorators at all.
|
|
520
|
-
|
|
521
|
-
Before generating relationships, group `table.foreignKeys` entries by `constraintName`.
|
|
522
|
-
One group is one FK relationship; rows in the same group are the ordered column pairs of a composite FK.
|
|
523
|
-
Preserve their metadata order.
|
|
524
|
-
|
|
525
|
-
**`@one` — one per FK constraint on this table.** For each FK constraint group:
|
|
526
|
-
|
|
527
|
-
- All rows must reference the same schema and table. If they do not, report inconsistent metadata and skip the constraint.
|
|
528
|
-
- `fieldName = camelCase(singularize(referencedTableName))`. If more than one constraint on the table would produce the same field name, derive a stable disambiguated name from `constraintName` rather than overwriting one.
|
|
529
|
-
- `sourceFields = group.map(fk => camelCase(fk.columnName))`.
|
|
530
|
-
- `targetFields = group.map(fk => camelCase(fk.referencedColumnName))`.
|
|
531
|
-
- The relationship is optional when any source column in the group is nullable. Emit `{ optional: true }` and `?:` in that case.
|
|
532
|
-
|
|
533
|
-
```ts
|
|
534
|
-
@one(() => <ReferencedClass>, { sourceFields: ['<src1>', '<src2>'], targetFields: ['<tgt1>', '<tgt2>'] })
|
|
535
|
-
<fieldName>!: <ReferencedClass>;
|
|
536
|
-
|
|
537
|
-
// Optional relationship
|
|
538
|
-
@one(() => <ReferencedClass>, { optional: true, sourceFields: ['<src>'], targetFields: ['<tgt>'] })
|
|
539
|
-
<fieldName>?: <ReferencedClass>;
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
Self-referencing FK constraints follow the same rules and must be optional.
|
|
543
|
-
Use the current class directly in the resolver and add no sibling import.
|
|
544
|
-
The entity and DAB relationship generate fine, but the client does not currently support selecting a dotted path across a self-relationship.
|
|
545
|
-
|
|
546
|
-
If the referenced table is not in the table index, skip the relationship and warn: `Foreign key <table>.<column> references unknown table <refSchema>.<refTable>; relationship skipped.`
|
|
547
|
-
|
|
548
|
-
In subset mode, apply the same skip when the referenced table exists in `metadata.json` but is not in the set you are generating: omit the `@one` and its sibling import, and warn that the relationship was dropped because the target is out of scope.
|
|
549
|
-
Never import a `./<Class>.js` file you did not write.
|
|
550
|
-
|
|
551
|
-
**`@many` — reverse-direction relationships pointing at this table.**
|
|
552
|
-
Build a reverse-FK index across all tables: every grouped FK constraint from another table to this table becomes one `@many` on this table.
|
|
553
|
-
|
|
554
|
-
- `fieldName = camelCase(pluralize(otherTable.tableName))` — `pluralize` is idempotent, so an already-plural table (`Orders`) stays `orders`, never `orderses`.
|
|
555
|
-
- `sourceFields = group.map(fk => camelCase(fk.referencedColumnName))` — this table's referenced properties.
|
|
556
|
-
- `targetFields = group.map(fk => camelCase(fk.columnName))` — the other table's FK properties.
|
|
557
|
-
|
|
558
|
-
```ts
|
|
559
|
-
@many(() => <OtherClass>, { sourceFields: ['<src>'], targetFields: ['<tgt>'] })
|
|
560
|
-
<fieldName>!: <OtherClass>[];
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
In subset mode, only emit a `@many` when the other table is also being generated.
|
|
564
|
-
|
|
565
|
-
`singularize` / `pluralize` are the same simplified rules the generator uses:
|
|
566
|
-
|
|
567
|
-
- `singularize`: `ies → y` (length > 3); `xes|ses|ches|shes → drop -es`; `<non-s>s → drop trailing -s`; else unchanged.
|
|
568
|
-
- `pluralize` (idempotent): if the name is already plural (ends in `s`, `es`, or `ies`), return it unchanged; otherwise `<non-vowel>y → -ies`; `x|z|ch|sh → +es`; else `+s`.
|
|
569
|
-
|
|
570
|
-
### 8. Missing key and relationship metadata
|
|
571
|
-
|
|
572
|
-
When `primaryKeyColumns` is absent or empty, emit one warning to the user: `No PK metadata available for <tableName>; generated as a keyless entity.`
|
|
573
|
-
|
|
574
|
-
When `foreignKeys` is absent or empty and no reverse FK constraint points at the table, emit one warning: `No FK metadata available for <tableName>; relationships omitted.`
|
|
575
|
-
|
|
576
|
-
For Lakehouse, these warnings describe a known metadata limitation, not a request to infer schema.
|
|
577
|
-
The entity file still generates with `primaryKey: []` and without `@one`/`@many`.
|
|
578
|
-
If the Builder knows logical keys or relationships the endpoint does not expose, explain that they may add those declarations manually; never add them without that explicit input.
|
|
579
|
-
|
|
580
|
-
### 9. Imports
|
|
581
|
-
|
|
582
|
-
Build the import line deterministically:
|
|
583
|
-
|
|
584
|
-
- Always include `entity` and `Source`.
|
|
585
|
-
- Then append, in this exact order, any decorator names actually used in the file: `boolean`, `date`, `decimal`, `int`, `text`, `uuid`, `one`, `many`. Skip any not used.
|
|
586
|
-
- All from `@microsoft/rayfin-core`.
|
|
587
|
-
- If any column is server-generated, also add `import type { AutoGenerated } from '@microsoft/rayfin-core';`.
|
|
588
|
-
- For every relationship target that is a different class, add a sibling import: `import { <SiblingClass> } from './<SiblingClass>.js';`. Sibling imports are alphabetised; self-references get no sibling import.
|
|
589
|
-
- In subset mode, only relationships that survived the subset skip contribute sibling imports.
|
|
590
|
-
|
|
591
|
-
### 10. Canonical example
|
|
592
|
-
|
|
593
|
-
```ts
|
|
594
|
-
// @generated — do not edit.
|
|
595
|
-
|
|
596
|
-
import { entity, uuid, text, int, date, one, many, Source } from '@microsoft/rayfin-core';
|
|
597
|
-
import type { AutoGenerated } from '@microsoft/rayfin-core';
|
|
598
|
-
import { Category } from './Category.js';
|
|
599
|
-
import { OrderItem } from './OrderItem.js';
|
|
600
|
-
|
|
601
|
-
@entity()
|
|
602
|
-
export class Product extends Source({ schema: 'dbo', table: 'Product', primaryKey: ['productId'] }) {
|
|
603
|
-
@uuid({ column: 'ProductID' })
|
|
604
|
-
productId!: string;
|
|
605
|
-
|
|
606
|
-
@text()
|
|
607
|
-
name!: string;
|
|
608
|
-
|
|
609
|
-
@int()
|
|
610
|
-
stock!: number;
|
|
611
|
-
|
|
612
|
-
// DEFAULT sysutcdatetime() — server-generated, so AutoGenerated: optional on
|
|
613
|
-
// write, read back in the returned row.
|
|
614
|
-
@date({ column: 'CreatedUtc' })
|
|
615
|
-
createdUtc!: AutoGenerated<Date>;
|
|
616
|
-
|
|
617
|
-
@one(() => Category, { sourceFields: ['categoryId'], targetFields: ['categoryId'] })
|
|
618
|
-
category!: Category;
|
|
619
|
-
|
|
620
|
-
@many(() => OrderItem, { sourceFields: ['productId'], targetFields: ['productId'] })
|
|
621
|
-
orderItems!: OrderItem[];
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
## metadata.json reference
|
|
626
|
-
|
|
627
|
-
Path: `rayfin/connectors/<name>/metadata.json`.
|
|
628
|
-
Written by `connector add`; never edit by hand.
|
|
629
|
-
It is regenerated only by `rayfin connector remove <name>` followed by `rayfin connector add ...`.
|
|
630
|
-
|
|
631
|
-
Top-level: `SchemaMetadata { source, connector, connectionString, discoveredAt, schemas[] }`.
|
|
632
|
-
Each schema entry: `{ schemaName, tables[] }`.
|
|
633
|
-
Each table: `{ tableName, columns[], foreignKeys?, primaryKeyColumns? }`.
|
|
634
|
-
|
|
635
|
-
Columns carry `columnName`, `dataType`, `isNullable`, and optional `maxLength` / `precision` / `scale`.
|
|
636
|
-
Server-generated columns additionally carry `identity` (`{ seed, increment }`), `default` (the SQL default expression), `computed` (the `AS (...)` expression), `serverManaged`, and `datePrecision`.
|
|
637
|
-
Foreign keys carry `constraintName`, `columnName`, and the `referencedTableSchema` / `referencedTableName` / `referencedColumnName` triple.
|
|
638
|
-
|
|
639
|
-
`primaryKeyColumns`, when available, contains single-column or composite PKs in `ORDINAL_POSITION` order.
|
|
640
|
-
Multiple `foreignKeys` entries with the same `constraintName` form one composite FK and must be generated as one relationship.
|
|
641
|
-
|
|
642
|
-
Fabric SQL Database generally exposes PK/FK and server-generation metadata.
|
|
643
|
-
Warehouse and Lakehouse may omit some catalog metadata; Lakehouse commonly omits PK/FK constraints entirely.
|
|
644
|
-
Absence means "unknown / not exposed", not permission to synthesize keys or relationships.
|
|
645
|
-
|
|
646
|
-
## Category A anti-patterns
|
|
647
|
-
|
|
648
|
-
- Never leave `schema.ts` as bare re-exports — the client import of `<Name>Schema` and `connectorConfig` would fail.
|
|
649
|
-
- Never import or re-export entity classes as **values** in `schema.ts`. Use `import type` / `export type`; a value import ships decorated classes to the browser and the page renders blank.
|
|
650
|
-
- Populate `connectorConfig.entities` with **column-name arrays**, never entity classes. Omitting it makes every no-selection read throw `SELECTION_REQUIRED`; using classes puts them back in the bundle.
|
|
651
|
-
- In subset mode, list in `TSchema` only entities you actually generated.
|
|
652
|
-
- Keep `connectorConfig.operations` identical to the YAML `operations:`, and keep each entity `@role(...)` action a subset of that pair. Narrow YAML first; never widen above the type's catalog allowlist, and never widen a decorator just to match the connector.
|
|
653
|
-
- Policies use the typed `claims` / `item` DSL — never raw SQL or DAB-policy strings like `"@claims.sub eq @item.owner_id"`.
|
|
654
|
-
- Never double-pluralize an entity/class name.
|
|
655
|
-
- GraphQL type names are global across connectors — disambiguate duplicates, never blanket-prefix.
|
|
656
|
-
- `metadata.json` is the only source of truth for keys and relationships. Never synthesize a primary key or infer a relationship from column names, sampled values, or naming conventions — absent metadata means keyless and relationship-free.
|
|
657
|
-
- When the user asks for one entity, read `metadata.json` and filter — do not regenerate every table.
|
|
658
|
-
- Never edit `metadata.json` or `dab-config.json` by hand — both are regenerated.
|
|
659
|
-
|
|
660
|
-
## Troubleshooting
|
|
661
|
-
|
|
662
|
-
| Symptom | Likely cause | Fix |
|
|
663
|
-
| --- | --- | --- |
|
|
664
|
-
| `rayfin up connector apply` fails on a role action | Entity `@role(...)` includes an action not in the YAML `operations:` | Narrow the decorator to match YAML. |
|
|
665
|
-
| `rayfin up connector apply` fails with a duplicate / redefined GraphQL type | Two connectors generated an entity with the same name — type names are global | Disambiguate the colliding entity by prefixing its source database name; update the class, file name, `@entity` name, `TSchema` key, and access path together, then re-apply. |
|
|
666
|
-
| `rayfin connector add` writes the YAML entry but no entity files | Expected — the CLI never emits entity `.ts` files | Generate them from `metadata.json` per the contract. If `metadata.json` is also missing, schema discovery failed. |
|
|
667
|
-
| `Property '<name>' does not exist on connectors` | Connector key in `AppConnectorsSchema` does not match the key in the `connectors` option | Use the `rayfin.yml` `name` in all three places. |
|
|
668
|
-
| A CRUD method is missing from autocomplete | Expected: `<Name>Schema` narrows methods to the connector's `operations` | Widen the allow-list via `rayfin connector add --operations`, then re-import. |
|
|
669
|
-
| Import of `ConnectorsRayfinClient` fails to resolve | Imported from the stable `@microsoft/rayfin-client` entry | Import from `@microsoft/rayfin-client/experimental`. |
|
|
670
|
-
| `Cannot find module '@microsoft/rayfin-connector-fabric-graphql'` | The connector packages were never installed — `connector add` does not add them | Run the pinned `npm install` command `connector add` printed, or rebuild it from `rayfin connector types --json`. |
|
|
671
|
-
| Deployed page is blank with `Uncaught SyntaxError: Invalid or unexpected token`, though build, tests and deploy all passed | `schema.ts` imports or re-exports entity classes as values, so the decorated classes were bundled into the browser build | Switch every entity import and re-export in `schema.ts` to `import type` / `export type`, and give `connectorConfig.entities` column-name arrays instead of classes. |
|
|
672
|
-
| A read throws `SELECTION_REQUIRED` | `connectorConfig.entities` is missing, so there is no default column list | Add the entity's property names, or pass an explicit `select([...])`. |
|
|
673
|
-
| A read returns null or errors on a field that exists in the database | `entities` lists database column names instead of the entity's property names | Use the names declared on the generated class, for example `productId`, not `ProductID`. |
|
|
7
|
+
> **Moved.** This reference now ships **inside the connector package**,
|
|
8
|
+
> version-locked to the connector the Builder actually installed — so per-dialect
|
|
9
|
+
> read/write behavior, SQL-type mappings, and composite-PK semantics can never
|
|
10
|
+
> drift from the CLI. It is fetched on demand with `packageVersion` provenance.
|
|
11
|
+
>
|
|
12
|
+
> Read it from `@microsoft/rayfin-connector-fabric-graphql`:
|
|
13
|
+
>
|
|
14
|
+
> - MCP: `search_docs` / `discover_packages`
|
|
15
|
+
> - CLI: `rayfin docs search "<term>"` (module `rayfin-connector-fabric-graphql`)
|
|
16
|
+
> - Source: `@microsoft/rayfin-connector-fabric-graphql/assets/docs/`
|
|
17
|
+
>
|
|
18
|
+
> Pages: `index.md` (generation workflow, the aggregate `schema.ts`, client
|
|
19
|
+
> wiring, worked example), `entities.md` (`@role` row-level policies, the entity
|
|
20
|
+
> generation contract, the `metadata.json` reference), `querying.md`,
|
|
21
|
+
> `mutations.md`, and `troubleshooting.md`.
|
|
@@ -11,7 +11,7 @@ The Builder never writes or sees the function code.
|
|
|
11
11
|
The Builder declares the connector and calls a typed method; the Fabric app backend injects connector configuration and delegated authentication before forwarding to the function.
|
|
12
12
|
There are no GraphQL entities, so do **not** generate entity files, `@role` policies, or `metadata.json` entities for these connector types.
|
|
13
13
|
|
|
14
|
-
For the entity-generating types (`fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`), read
|
|
14
|
+
For the entity-generating types (`fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`), read the package-owned Category A docs in `@microsoft/rayfin-connector-fabric-graphql` (`rayfin docs search` / `search_docs`) instead.
|
|
15
15
|
|
|
16
16
|
## What each type exposes
|
|
17
17
|
|
|
@@ -35,7 +35,7 @@ Each command has its own reference page:
|
|
|
35
35
|
|
|
36
36
|
Each category has its own contract page — read the one that matches your connector type, not both:
|
|
37
37
|
|
|
38
|
-
- [Category A — GraphQL entity connectors](./category-a-entities.md) — entity generation, `@role` policies, the aggregate schema, and the per-dialect read/write matrix.
|
|
38
|
+
- [Category A — GraphQL entity connectors](./category-a-entities.md) — now **package-owned**; the full reference (entity generation, `@role` policies, the aggregate schema, and the per-dialect read/write matrix) ships in `@microsoft/rayfin-connector-fabric-graphql` (`rayfin docs search` / `search_docs`).
|
|
39
39
|
- [Category B — function-bridge connectors](./category-b-function-bridge.md) — the `kusto` and `fabric-semanticmodel` contract, including the Kusto cluster routing baked into the generated `schema.ts`.
|
|
40
40
|
|
|
41
41
|
A typical loop is search → add → inspect (Category A) or search → add → invoke (Category B).
|
|
@@ -175,10 +175,6 @@ Configure an email provider for magic links, password resets, and email verifica
|
|
|
175
175
|
| `folder` | `string` | `"dist"` | Directory containing built static assets (relative to `root`). |
|
|
176
176
|
| `buildCommand` | `string` | — | Shell command to run before packaging (e.g. `npm run build`). |
|
|
177
177
|
| `indexDocument` | `string` | — | Default document served for the root path (e.g. `index.html`). |
|
|
178
|
-
| `anonymousAccess` | `boolean` | — | Whether visitors can open the app without signing in. Required when static hosting is enabled. |
|
|
179
|
-
|
|
180
|
-
`anonymousAccess` has no default.
|
|
181
|
-
See [Static hosting](../hosting/index.md) for the posture table.
|
|
182
178
|
|
|
183
179
|
> **Tip:** All string values support environment variable interpolation with `${VAR}` and `${VAR:-default}` syntax.
|
|
184
180
|
> Variables are resolved from `rayfin/.env` and the shell environment.
|
|
@@ -37,36 +37,6 @@ services:
|
|
|
37
37
|
| `root` | No | Project root | Root directory of the frontend project, relative to the project root. |
|
|
38
38
|
| `buildCommand` | No | — | Shell command to run before packaging (for example, `npm run build`). |
|
|
39
39
|
| `indexDocument` | No | — | Default document to serve for directory requests (for example, `index.html`). |
|
|
40
|
-
| `anonymousAccess` | Yes | — | Whether visitors can open the app without signing in. |
|
|
41
|
-
|
|
42
|
-
### Access posture
|
|
43
|
-
|
|
44
|
-
`anonymousAccess` selects who can open your deployed app.
|
|
45
|
-
Set `anonymousAccess` explicitly — leaving it out is not the same as setting it to `false`, and once your tenant enforces static-hosting access control a deployment that omits `anonymousAccess` is rejected.
|
|
46
|
-
|
|
47
|
-
| Posture | `anonymousAccess` | Who can open the app |
|
|
48
|
-
| --- | --- | --- |
|
|
49
|
-
| Signed-in users only | `false` | Visitors must sign in. |
|
|
50
|
-
| Public | `true` | Anyone with the link, no sign-in. |
|
|
51
|
-
|
|
52
|
-
```yaml
|
|
53
|
-
services:
|
|
54
|
-
staticHosting:
|
|
55
|
-
enabled: true
|
|
56
|
-
folder: dist
|
|
57
|
-
anonymousAccess: false
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`rayfin up` asks you to choose a posture the first time it deploys a project that has not recorded one, then writes your choice to `rayfin.yml` so later deploys never ask again.
|
|
61
|
-
In automation — `--yes`, a non-interactive terminal, or `CI=true` — it never prompts: add `anonymousAccess` to `rayfin.yml` and re-run.
|
|
62
|
-
`anonymousAccess` must be `true` or `false`; a value that is neither, such as an environment variable that did not resolve, is rejected before anything is deployed.
|
|
63
|
-
|
|
64
|
-
A newly scaffolded project already has `anonymousAccess: false`, so it is private from the moment it exists and deploys without a prompt.
|
|
65
|
-
The prompt exists for projects created before this property did: those apps are being served publicly today, so `rayfin up` asks once and never guesses on your behalf.
|
|
66
|
-
Choosing public does not turn off Rayfin auth — it decides who may load the site, not whether the app can sign users in.
|
|
67
|
-
|
|
68
|
-
`rayfin up staticapp deploy` does not set an access posture.
|
|
69
|
-
It publishes content only, so the app keeps whatever posture the last full `rayfin up` recorded.
|
|
70
40
|
|
|
71
41
|
### Declared package versions
|
|
72
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/rayfin-guide",
|
|
3
|
-
"version": "1.36.0-alpha.
|
|
3
|
+
"version": "1.36.0-alpha.1588",
|
|
4
4
|
"description": "Cross-cutting Builder guides for the Rayfin platform — discovered by `@microsoft/rayfin-docs` via the `rayfinDocs` package.json field convention.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|