[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M70HMFPA9WXWY5X7C14XB11D":3,"skill-same:01M70HMFPA9WXWY5X7C14XB11D":79},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":57,"commitSha":58,"skillDirectory":59,"forksCount":60,"repositoryHtmlUrl":61,"repositoryHomepageUrl":6,"ownerHtmlUrl":62,"ownerBlogUrl":63,"ownerDescription":64,"updatedAt":65,"install":66,"resources":78},"01M70HMFPA9WXWY5X7C14XB11D","rayfin-connectors",null,"Use for Rayfin connectors that wire existing Fabric SQL data sources (Lakehouse, Warehouse, SQL Database) into an app as GraphQL-backed entities. Triggers: rayfin connector, rayfin up connector apply, connectors:, fabric-sqlanalytics, fabric-warehouse, fabric-sqldatabase, kusto, kql, eventhouse, workspaceId, itemId, operations:, AppConnectorsSchema, ConnectorsSchema, rayfin\u002Fconnectors\u002F, metadata.json, connector @role policy, connector claims.sub filter, connector claims.email filter, ConnectorsRayfinClient, rayfin-client.ts, client.connectors, connectorConfig, GraphQLBackedConnector, RestrictedDataApi, schema.ts aggregate, generate entities from metadata, add a use case on specific entities, wire connector into client, call a connector from the SDK, rayfin connector inspect, inspect connector data, sample connector rows, fabric-semanticmodel, --entity, --query, DAX EVALUATE, read-only sample query, rayfin connector search, discover connectable sources, find fabric sources to add, --all-workspaces, --workspace-id, connector search interactive picker, rayfin connector invoke, invoke a connector operation, run executeQuery, --input, --file, RAYFIN_FEATURE_FLAGS, resultSetRowCountLimit, limit rows, row limit, cap rows, too many rows, overflow","microsoft\u002Fawesome-rayfin","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69BFJTNT39H6W8XT2H1BZDN",112,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Frayfin-connectors\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"rayfin","connectors","rayfin connectors","for","email","limit","kql","as","input","read","run","data sources","generate","cap","filter","invoke","search","workspaces","inspect","app","2026-11-10 10:12:30",false,"---\nname: rayfin-connectors\ndescription: \"Use for Rayfin connectors that wire existing Fabric SQL data sources (Lakehouse, Warehouse, SQL Database) into an app as GraphQL-backed entities. Triggers: rayfin connector, rayfin up connector apply, connectors:, fabric-sqlanalytics, fabric-warehouse, fabric-sqldatabase, kusto, kql, eventhouse, workspaceId, itemId, operations:, AppConnectorsSchema, ConnectorsSchema, rayfin\u002Fconnectors\u002F, metadata.json, connector @role policy, connector claims.sub filter, connector claims.email filter, ConnectorsRayfinClient, rayfin-client.ts, client.connectors, connectorConfig, GraphQLBackedConnector, RestrictedDataApi, schema.ts aggregate, generate entities from metadata, add a use case on specific entities, wire connector into client, call a connector from the SDK, rayfin connector inspect, inspect connector data, sample connector rows, fabric-semanticmodel, --entity, --query, DAX EVALUATE, read-only sample query, rayfin connector search, discover connectable sources, find fabric sources to add, --all-workspaces, --workspace-id, connector search interactive picker, rayfin connector invoke, invoke a connector operation, run executeQuery, --input, --file, RAYFIN_FEATURE_FLAGS, resultSetRowCountLimit, limit rows, row limit, cap rows, too many rows, overflow\"\nmetadata:\n  author: microsoft\n  version: 0.5.1\nrayfin-managed: true\n---\n# Rayfin Connectors\n\nBring an existing Fabric SQL data source (Lakehouse, Warehouse, SQL Database) into a Rayfin app as typed, GraphQL-backed entities. Covers the three day-one connector types, the lifecycle commands, generating entity files from a discovered schema, and scoping the surface with YAML `operations:` and `@role(...)`.\n\n## When to Use a Connector\n\nUse a connector when the user wants the app to read or write **existing** SQL data already living in a Fabric workspace (a Lakehouse, Warehouse, or SQL Database).\n\nDo **not** use a connector when the user is creating a brand-new data model owned by the Rayfin app — those entities go under `rayfin\u002Fdata\u002F` and are managed via `rayfin up db apply`, not `rayfin connector add`.\n\n**Routing note:** `@role` and `@entity` also appear in the base `rayfin` skill for owned `rayfin\u002Fdata\u002F` models. This skill applies only when those decorators are used inside `rayfin\u002Fconnectors\u002F` files, or when the context is connector-level RLS policies and entity generation from a Fabric source.\n\nConnectors are cloud-only on day-one.\n`rayfin dev` parses the `connectors:` block but does not wire it; verifying connector behavior requires `rayfin up`.\n\n**Prerequisite — the `connector` command group is feature-flagged.**\n`rayfin connector ...` is only registered when `RAYFIN_FEATURE_FLAGS` contains `connectors`.\nWithout it the commands do not exist and the CLI reports an unknown command.\n\n**Two categories — decide first.**\n\n- **Category A — GraphQL-backed entities.** Types `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase` surface Fabric SQL as typed entities with read\u002Fcreate\u002Fupdate\u002Fdelete operations and `@role` row-level security.\n  Use the [Step 1–8 workflow](#workflow).\n- **Category B — function-bridge operations.** Type `kusto` (Fabric KQL Database) surfaces a platform-owned function as the named operations `executeQuery` (a KQL query) and `executeCommand` (a Kusto management\u002Fcontrol command); type `fabric-semanticmodel` (DAX) surfaces `executeQuery`. Both run as the signed-in user.\n  Do not generate entities, `@role` policies, or `metadata.json` entities.\n  Use the [Category B section](#category-b--function-bridge-connectors) below and skip Steps 4–6.\n\n## Connector Type Matrix\n\n| Type | Display | Operations the type _can_ support | Auth modes | Required config | Notes |\n|---|---|---|---|---|---|\n| `fabric-sqlanalytics` | Lakehouse SQL endpoint | `read` | `delegated`, `application` | `workspaceId`, `itemId` | Read-only at the host. |\n| `fabric-warehouse` | Warehouse | `read`, `create`, `update`, `delete` | `delegated`, `application` | `workspaceId`, `itemId` | Full CRUD. |\n| `fabric-sqldatabase` | SQL Database | `read`, `create`, `update`, `delete` | `delegated`, `application` | `workspaceId`, `itemId` | Full CRUD. |\n| `fabric-semanticmodel` | Fabric semantic model | `executeQuery` | `delegated` | `workspaceId`, `itemId` | Category B function bridge for DAX; adapter version `1`. |\n| `kusto` | Fabric KQL Database | `executeQuery`, `executeCommand` | `delegated` | `workspaceId`, `itemId` | Category B function bridge for KQL queries and management commands; adapter version `1`. |\n\nThe validator rejects any `operations:` entry not in the connector type's allowlist.\nThe Lakehouse read-only restriction is enforced server-side.\n\n## Category B — Function-Bridge Connectors\n\nA Category B connector is a named-operation surface backed by a small, platform-owned function (UDF).\nThe Builder never writes or sees the function code.\nThe Builder declares the connector and calls a typed method; the Fabric app backend injects connector configuration and delegated authentication before forwarding to the function.\nThere are no GraphQL entities, so do not generate entity files, `@role` policies, or `metadata.json` entities for these connector types.\n\n### Add the connector\n\n```bash\nrayfin connector add \\\n  --type kusto \\\n  --workspace-id \u003Cws-id> \\\n  --item-id \u003Ckql-database-item-id> \\\n  --name telemetry\n```\n\n`--item-id` identifies the Fabric item to query: a KQL Database for `kusto`.\nOmit `--name` to derive the connector name from the Fabric item's display name.\nThe CLI verifies the item, writes the `rayfin.yml` entry, and scaffolds `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts`.\nThe Kusto cluster query endpoint and database are resolved server-side from `(workspaceId, itemId)` at call time.\nNever write a cluster URI or database name to YAML, and never send either value from app code.\n\n### Resulting rayfin.yml\n\n```yaml\nconnectors:\n  - name: telemetry\n    type: kusto\n    version: '1'\n    config:\n      workspaceId: \u003Cws-id>\n      itemId: \u003Ckql-database-item-id>\n    auth:\n      type: delegated\n    operations:\n      - name: executeQuery\n      - name: executeCommand\n```\n\nThe `config` block is the single source of truth for what to query and is not sent on the client wire.\nCategory B connectors are pinned to an adapter version.\n`kusto` allows `executeQuery` and `executeCommand`; `fabric-semanticmodel` allows `executeQuery`. In every case `auth.type` must be `delegated`.\nDelegated authentication uses the signed-in user's identity through the on-behalf-of flow.\n\n### Typed schema\n\nThe CLI-generated `rayfin\u002Fconnectors\u002Ftelemetry\u002Fschema.ts` exports both the typed marker and the runtime connector configuration:\n\n```ts\nimport type { ConnectorConfig } from '@microsoft\u002Frayfin-connectors';\nimport type { Kusto } from '@microsoft\u002Frayfin-connector-kusto';\n\nexport type TelemetrySchema = Kusto\u003C'executeQuery' | 'executeCommand'>;\n\nexport const connectorConfig = {\n  connector: 'kusto',\n} as const satisfies ConnectorConfig;\n```\n\nInstall both imported packages as direct dependencies so the generated file resolves under strict package managers:\n\n```bash\nnpm install @microsoft\u002Frayfin-connector-kusto @microsoft\u002Frayfin-connectors\n```\n\n### Wire into the aggregate schema and client\n\nUse the same `AppConnectorsSchema` and `ConnectorsRayfinClient` wiring pattern as Step 8, but do not add entity re-exports or a `GraphQLBackedConnector`.\nImport the generated `\u003CName>Schema` type and `connectorConfig` value, key both maps by the exact `rayfin.yml` connector name, and pass the config through the client's `connectors` option:\n\n```ts\nimport { ConnectorsRayfinClient } from '@microsoft\u002Frayfin-client\u002Fexperimental';\nimport {\n  type TelemetrySchema,\n  connectorConfig as telemetryConfig,\n} from '..\u002F..\u002Frayfin\u002Fconnectors\u002Ftelemetry\u002Fschema';\n\ntype AppConnectorsSchema = {\n  telemetry: TelemetrySchema;\n};\n\nconst client = new ConnectorsRayfinClient\u003C\n  Record\u003Cstring, never>,\n  Record\u003Cstring, never>,\n  AppConnectorsSchema\n>({\n  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,\n  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,\n  connectors: {\n    telemetry: telemetryConfig,\n  },\n});\n```\n\n### Call it from app code\n\nCorrelation ids are not part of the response body (the connector function relays\nthe Kusto bytes untouched), so generate the `clientRequestId` yourself and pass\nthe same value to both `executeQuery` and `toQueryResult`:\n\n```ts\nconst clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`;\n\nconst response = await client.connectors.telemetry.executeQuery({\n  query: 'StormEvents | summarize count() by State | top 10 by count_',\n  clientRequestId,\n});\n```\n\nOptionally normalize the raw Kusto v1 `{ Tables }` document into a discriminated\nresult that preserves every returned Kusto table:\n\n```ts\nimport { toQueryResult } from '@microsoft\u002Frayfin-connector-kusto';\n\nconst result = toQueryResult(response, { clientRequestId });\nif (result.status === 'success') {\n  renderTables(result.tables);\n} else {\n  showError(result.error.code, result.error.message);\n}\n```\n\nSuccessful results contain `tables` plus the `clientRequestId` you passed in\n(empty when you pass none) and an optional `activityId`; correlation is never on\nthe wire.\nEach table contains named typed `columns` and row-major `rows`.\nError results contain `error.message` and an optional `error.code`.\n\n#### Management commands (`kusto`)\n\n`executeCommand` runs a Kusto management (control) command — the command text\nstarts with a leading dot. It returns the same native Kusto v1 `{ Tables }`\ndocument as `executeQuery`, so normalize it with `toQueryResult` the same way,\nand pass a matching `clientRequestId` to correlate end to end:\n\n```ts\nconst clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`;\n\nconst databases = await client.connectors.telemetry.executeCommand({\n  command: '.show databases',\n  clientRequestId,\n});\n\nconst result = toQueryResult(databases, { clientRequestId });\n```\n\n#### Limiting rows (`fabric-semanticmodel`)\n\n`executeQuery` on `fabric-semanticmodel` accepts an optional `resultSetRowCountLimit`. There is no default. Omit it and every row comes back, so ask the user for a bound rather than inventing one.\n\nPrefer it over wrapping the DAX in `TOPN` when the user wants a guard rather than a deliberately ranked subset. Exceeding it fails the query with an `'overflow'` error, so a truncated result announces itself, where a `TOPN` returns a complete-looking partial answer.\n\nRun `rayfin docs search \"resultSetRowCountLimit\"` for version-locked details, since this behavior is owned by the connector package rather than this skill.\n\n### Verify\n\n`rayfin dev` parses `connectors:` but does not wire Category B calls.\nA real `executeQuery` requires `rayfin up`.\n\n## Workflow\n\nThe Step 1–8 workflow below is for **Category A** GraphQL entity connectors.\nFor **Category B** (`kusto` and `fabric-semanticmodel`), use the Category B section above and skip Steps 4–6 for entity files, `@role`, and row-level security.\n\nFor any connector task, walk these steps in order.\nStop and ask the user when the next step needs information you don't already have.\n\n### Step 1 — Add the connector\n\n```bash\nrayfin connector add --type \u003Ctype> --workspace-id \u003Cws-id> --item-id \u003Citem-id> [--name \u003Cname>] [--operations \u003Cops>]\n```\n\nThe CLI verifies the Fabric item, derives a connector `name` from the item's display name (override with `--name`), writes the entry to `rayfin.yml`, scaffolds `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts`, and runs schema discovery.\n\nEither way, `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json` is written so a subset of entities can be generated later.\n\nOptions for `connector add`:\n\n| Flag | Required | Purpose |\n|---|---|---|\n| `--type \u003Ctype>` | yes | One of `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`. |\n| `--workspace-id \u003Cid>` | yes (Fabric) | Fabric workspace ID. Must be a literal — `${VAR}` placeholders are rejected. |\n| `--item-id \u003Cid>` | yes (Fabric) | Fabric item\u002Fartifact ID. Must be a literal. |\n| `--name \u003Cname>` | no | Connector name. Derived from the item display name when omitted. |\n| `--operations \u003Cops>` | no | Comma-separated subset of the type's allowed operations (e.g. `read,update`). Narrows the emitted `operations:` at add-time. Omit for all allowed operations. Each value must be in the type's catalog allowlist. |\n| `-y, --yes` | no | Auto-accept overwrite\u002Fconfirmation prompts (non-interactive). |\n| `-v, --verbose` | no | Verbose diagnostics. |\n\nPrefer `--operations` to scope the connector at add-time instead of hand-editing\nYAML afterwards (see Step 3); the [CLI Quick Reference](#cli-quick-reference) has a worked example.\n\n### Step 2 — Confirm the user's operation scope\n\n`connector add` writes **every** operation the catalog allows for the type.\nBefore doing anything else, confirm what the user actually needs and narrow `rayfin.yml` accordingly.\n\n| User intent | Operations to keep |\n|---|---|\n| Read-only, analytics, dashboard, \"browse\" | `read` |\n| Append-only log or event capture | `read`, `create` |\n| Users edit existing rows but cannot add new ones | `read`, `update` |\n| Full CRUD app | `read`, `create`, `update`, `delete` |\n\nIf the user's intent is ambiguous, ask explicitly — for example: \"Should the app be allowed to create, update, or delete `\u003Centity>`, or only read it?\".\n\nAlso ask whether rows are scoped per-user (e.g. an `owner_id` \u002F `tenant_id` \u002F `created_by` column suggests row-level security — see Step 6).\n\n### Step 3 — Narrow `operations:` in `rayfin.yml`\n\nIf you already scoped the connector with `--operations` at add-time (Step 1),\nthe YAML already lists only those operations — skip ahead. Otherwise, edit the\nconnector entry to list only the operations the user needs.\nOperations are objects, not bare strings:\n\n```yaml\nconnectors:\n  - name: inventory\n    type: fabric-warehouse\n    config:\n      workspaceId: ${WS_ID}\n      itemId: ${ITEM_ID}\n    auth:\n      type: delegated\n    operations:\n      - name: read\n      - name: update     # removed 'create' and 'delete' per user scope\n```\n\nRules:\n\n- You can narrow below the catalog default; you cannot widen above it.\n- There is no `all` meta-operation — list every action explicitly.\n- The host validator (`ConnectorsSettingsValidator`) rejects unknown or duplicate names at `rayfin up` time.\n\n### Step 4 — Generate entity files (you, not the CLI)\n\n`rayfin connector add` writes `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json` and a\nplaceholder `schema.ts`, then stops. The per-table entity `.ts` files are\n**produced by you** by reading `metadata.json` and following the contract in\n[Entity Generation Contract](#entity-generation-contract). The CLI does not\nemit them.\n\nWhat to do, in order, for every connector-add flow:\n\n1. Read `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json` (shape: `SchemaMetadata` —\n   see [metadata.json Reference](#metadatajson-reference)).\n2. Pick the tables in scope:\n   - **Full set** — every table under `schemas[].tables[]`.\n   - **Subset** — only the tables the user named. Filter\n     `schemas[].tables[]` by `tableName` before generating.\n3. For each surviving table, write a file at\n   `rayfin\u002Fconnectors\u002F\u003Cname>\u002F\u003CEntityName>.ts` following the\n   [Entity Generation Contract](#entity-generation-contract) exactly. The\n   contract covers naming (including cross-connector name-collision\n   disambiguation — scan the other `rayfin\u002Fconnectors\u002F*\u002F` directories first,\n   see [§1](#1-file-name-and-class-name)), type mapping, nullable handling,\n   relationships, primary-key declaration (single or composite),\n   synthetic-PK fallback, and import-line composition.\n4. Overwrite the placeholder `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts` with the\n   aggregate per [Step 7](#step-7--update-the-aggregate-connector-schema).\n5. Surface every warning the contract emits (no-FK-metadata notes, unknown SQL\n   types). Don't drop them silently.\n\nWhen the user asks for a refresh (\"regenerate everything\"), re-do the same flow\nagainst the existing `metadata.json` — only re-run `rayfin connector remove`\nand `rayfin connector add` when the _source schema_ changed and you need fresh\nmetadata (see [metadata.json Reference](#metadatajson-reference)).\n\n### Step 5 — Scope `@role(...)` on entities\n\nThe YAML `operations:` controls which actions reach the source.\nThe entity-level `@role(...)` decorator controls which roles can perform which actions on that entity.\n\nThe entity's `actions` list **must be a subset** of the YAML `operations:` for that connector.\nThe host's settings validator does not catch the mismatch today — DAB will fail at `rayfin up connector apply` time.\nAlways narrow YAML first, then add `@role(...)` to entities (full alignment rule: [Step 7](#step-7--update-the-aggregate-connector-schema)).\n\nExample: a `fabric-warehouse` connector with `operations: [read, update]` and an `Order` entity locked to read-only:\n\n```ts\nimport { entity, int, text, decimal, Source } from '@microsoft\u002Frayfin-core\u002Fexperimental';\nimport { role } from '@microsoft\u002Frayfin-core';\n\n@role('authenticated', ['read'])\n@entity()\nexport class Order extends Source({ schema: 'dbo', table: 'Order', primaryKey: ['orderId'] }) {\n  @int({ column: 'OrderID' }) orderId!: number;\n  @text() customerEmail!: string;\n  @decimal({ precision: 18, scale: 2 }) total!: number;\n}\n```\n\nLegal `actions`: `'read' | 'create' | 'update' | 'delete' | '*'`.\nStack multiple `@role(...)` decorators to give different roles different actions on the same entity.\n\n### Step 6 — Add row-level policies (optional)\n\nUse a `policy` callback on `@role(...)` for row-level security.\nPolicies use the typed `claims` \u002F `item` DSL — never raw SQL strings.\n\n```ts\n@role('authenticated', ['read', 'update'], {\n  policy: (claims, item) => claims.sub.eq(item.owner_id),\n})\n@entity()\nexport class Todo extends Source({ schema: 'dbo', table: 'Todo', primaryKey: ['id'] }) {\n  @uuid() id!: string;\n  @uuid() owner_id!: string;\n  @text() body?: string;\n}\n```\n\nDSL surface: `claims.sub | email | role`, `item.\u003CcolumnName>`, `.eq(...)`, `.and(...)`, `.or(...)`.\nUse any claim the same way — e.g. `claims.email.eq(item.user_email)`.\n`RoleDeclarationOptions` also accepts `include` and `exclude` arrays for field-level allow\u002Fblock lists.\n\nWhen to prompt the user: inspect `metadata.json` for columns named `owner_id`, `user_id`, `tenant_id`, `created_by`, etc.\nIf you see one, ask: \"Should `\u003Ccolumn>` restrict rows so each authenticated user only sees their own?\".\n\n### Step 7 — Update the aggregate connector schema\n\n**Install the connector packages first.** `rayfin connector add` scaffolds files but installs nothing, and the aggregate `schema.ts` imports two packages a fresh app does not yet declare. Add both as direct dependencies before writing the file, or the Step 8 type-check fails with `Cannot find module`:\n\n```bash\nnpm install @microsoft\u002Frayfin-connector-fabric-graphql @microsoft\u002Frayfin-connectors\n```\n\n- `@microsoft\u002Frayfin-connector-fabric-graphql` — provides `GraphQLBackedConnector` (not a dependency of `@microsoft\u002Frayfin-client`, so it is always missing on a fresh app).\n- `@microsoft\u002Frayfin-connectors` — provides `ConnectorConfig`. It ships transitively under `@microsoft\u002Frayfin-client`, but declare it directly so strict resolvers (pnpm) do not treat it as a phantom dependency.\n\nMatch the version line already used by `@microsoft\u002Frayfin-client` in the app's `package.json` (both are `1.x`). Even though both imports are `import type`, TypeScript still needs the packages present at compile time.\n\nThe aggregate `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts` is what the app imports from.\nThe CLI leaves a placeholder there; overwrite it so it exports **three** things:\n\n1. **Entity re-exports** — re-export every entity class you generated for this connector.\n2. **`\u003CName>Schema` (type)** — a `GraphQLBackedConnector\u003CTSchema, typeof connectorConfig>` marker from `@microsoft\u002Frayfin-connector-fabric-graphql`. `TSchema` maps each in-scope entity name to its class via `typeof`; the second argument is `typeof connectorConfig` (the config value below, declared `as const satisfies ConnectorConfig`), from which the marker derives both the CRUD operation union and the dialect. This is the typed surface that makes `client.connectors.\u003Cname>.\u003CEntity>.select(...)` strongly typed and gates CRUD methods to the declared operations at compile time.\n3. **`connectorConfig` (value)** — a `ConnectorConfig` from `@microsoft\u002Frayfin-connectors` carrying the runtime `connector` type, the same `operations` allow-list, and an **`entities` map** (each in-scope entity class keyed by its entity name). The runtime routes on `connector`, gates CRUD verbs by `operations`, and reads `entities` so `create` \u002F `update` \u002F `delete` can return the **full row** — including server-generated columns the caller never sent (see [Writes](#writes--gated-by-operations-and-shaped-by-dialect)). **Always include `entities`** in a generated connector's config, keyed exactly as in `TSchema` and covering every entity there — it is a required part of the contract: `create` \u002F `update` \u002F `delete` and no-selection reads depend on it to shape and return the full row. Include it for every dialect so the config is complete and correct.\n\n`GraphQLBackedConnector\u003CTSchema, typeof connectorConfig>` **is** the published typed marker for Category A SQL connectors — use it (see Rules for the full marker contract).\n\n**Single source of truth for operations.**\nFor one connector, the `connectorConfig.operations` array here (which the marker reads via `typeof connectorConfig`), the YAML `operations:` (Step 3), and every entity `@role(...)` action (Step 5) must all describe the same verb set.\nNarrow YAML first, then mirror it into the marker, the config, and the decorators.\n\n**Subset rule.**\nIn subset mode (Step 4), include in `TSchema` **only** the entities you actually generated.\nNever reference an entity class you did not generate — the `typeof` lookup and its import would dangle.\n\n```ts\n\u002F\u002F rayfin\u002Fconnectors\u002Finventory\u002Fschema.ts\nimport type { GraphQLBackedConnector } from '@microsoft\u002Frayfin-connector-fabric-graphql';\nimport type { ConnectorConfig } from '@microsoft\u002Frayfin-connectors';\n\nimport { Order } from '.\u002FOrder.js';\nimport { Customer } from '.\u002FCustomer.js';\n\n\u002F\u002F 1. Re-export the generated entities.\nexport { Order } from '.\u002FOrder.js';\nexport { Customer } from '.\u002FCustomer.js';\n\n\u002F\u002F 2. Runtime routing\u002Fgating config. Consumed by the `connectors` client option\n\u002F\u002F    and, via `typeof connectorConfig`, by the marker below. Use `as const\n\u002F\u002F    satisfies` (not a `: ConnectorConfig` annotation) so the `connector` and\n\u002F\u002F    `operations` literals survive — the marker reads them to derive the\n\u002F\u002F    permitted operations and the dialect (which selects the mutation return\n\u002F\u002F    type: `DbOperationResult` for Fabric Warehouse, the entity row otherwise).\n\u002F\u002F    `entities` maps each in-scope entity class by name so create\u002Fupdate\u002Fdelete\n\u002F\u002F    return the full row (SQL Database); include every entity in `TSchema`.\nexport const connectorConfig = {\n  connector: 'fabric-warehouse',\n  operations: ['read', 'update'],\n  entities: { Order, Customer },\n} as const satisfies ConnectorConfig;\n\n\u002F\u002F 3. Typed marker — entity map + the connector config. Consumed by\n\u002F\u002F    AppConnectorsSchema.\nexport type InventorySchema = GraphQLBackedConnector\u003C\n  { Order: typeof Order; Customer: typeof Customer },\n  typeof connectorConfig\n>;\n```\n\nThe `\u003CName>Schema` name is the PascalCase connector name plus `Schema` (connector `inventory` → `InventorySchema`).\nFor a read-only Lakehouse (`fabric-sqlanalytics`), `operations` is `['read']` (reads only; it never writes).\n\n### Step 8 — Wire connectors into the app client (`rayfin-client.ts`)\n\nDeclaring and deploying a connector does not make it callable from app code.\nThe Builder must expose it through a `ConnectorsRayfinClient` so it shows up as\n`client.connectors.\u003Cname>`.\n\n`ConnectorsRayfinClient` is **experimental** — import it only from the\n`@microsoft\u002Frayfin-client\u002Fexperimental` subpath, never the stable\n`@microsoft\u002Frayfin-client` entry.\n\nEach `schema.ts` exports the `\u003CName>Schema` type and the `connectorConfig` value\n(both defined in [Step 7](#step-7--update-the-aggregate-connector-schema)). The\n`\u003CName>Schema` type goes into `AppConnectorsSchema` so `client.connectors.\u003Cname>`\nis typed; the `connectorConfig` value goes into the `connectors` option so the\nruntime knows which client to build.\n\nThe connector **key must be identical** in three places: the `name` in `rayfin.yml`,\nthe property in `AppConnectorsSchema`, and the property in the `connectors` option.\n\nCreate or update `src\u002Flib\u002Frayfin-client.ts`. One import pair per connector (alias the value — every `schema.ts` exports it as `connectorConfig`); map each connector key to its `\u003CName>Schema` type in `AppConnectorsSchema` and to its config value in the `connectors:` option. Each `\u003CName>Schema` already carries the connector's config (via `typeof connectorConfig` in Step 7), so the dialect types the entity mutation returns — Fabric Warehouse mutations return `DbOperationResult`, others the entity row — with nothing extra to wrap here:\n\n```ts\nimport { ConnectorsRayfinClient } from '@microsoft\u002Frayfin-client\u002Fexperimental';\nimport { ProductDbSchema, connectorConfig as productDbConfig } from '..\u002F..\u002Frayfin\u002Fconnectors\u002FproductDb\u002Fschema';\nimport { InventorySchema, connectorConfig as inventoryConfig } from '..\u002F..\u002Frayfin\u002Fconnectors\u002Finventory\u002Fschema';\n\ntype AppConnectorsSchema = {\n  productDb: ProductDbSchema;\n  inventory: InventorySchema;\n};\n\nexport function getRayfinClient() {\n  \u002F\u002F Type params are \u003CDataSchema, FunctionsSchema, ConnectorsSchema> — set the\n  \u002F\u002F unused slots to Record\u003Cstring, never>, or pass real schema types if the app\n  \u002F\u002F also has a data model \u002F functions.\n  return new ConnectorsRayfinClient\u003CRecord\u003Cstring, never>, Record\u003Cstring, never>, AppConnectorsSchema>({\n    baseUrl: import.meta.env.VITE_RAYFIN_API_URL,\n    publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,\n    authStorage: true,\n    connectors: {\n      productDb: productDbConfig,\n      inventory: inventoryConfig,\n    },\n  });\n}\n```\n\nOnce wired, each connector is reached through `client.connectors.\u003Cname>`, which\nexposes entities narrowed to the connector's `operations`:\n\n```ts\nconst created = await getRayfinClient().connectors.productDb.Colors.create({\n  id: crypto.randomUUID(),\n  name: 'Crimson',\n  hexCode: '#DC143C',\n});\n```\n\n**Write results on Fabric Warehouse.** Fabric Warehouse (DWSQL) has no T-SQL\n`OUTPUT` clause, so it cannot read the written row back. `create` \u002F `update` \u002F\n`delete` on a `fabric-warehouse` connector therefore resolve to\n`DbOperationResult { result: string }` instead of the entity row; every other\nwrite dialect (`fabric-sqldatabase`) returns the written row. So `created` above\nis the written row on SQL Database and a `DbOperationResult` status object on a\nwarehouse — `result` is `\"success\"` on a successful write, while a failed write\nthrows a GraphQL error rather than returning a `\"failure\"` result.\n\n**Querying connector entities.** Reach entities through `client.connectors.\u003Cname>.\u003CEntity>`. Reads use the query chain `.select()` → `.where()` → `.orderBy()` → `.execute()`, plus `findByKey` (pass a key object with all composite parts **and** a required scalar-only `select`) and pagination. The connector's `operations:` gate the surface, and the dialect shapes write returns — see [Reading & writing connector entities](#reading--writing-connector-entities-per-dialect) for the full per-dialect matrix. For aggregation, `groupBy`, or year\u002Fquarter\u002Fmonth time-bucketing, use `search_docs(...)` \u002F `rayfin docs`.\n\nVerify: type-check the project (`tsc --noEmit` or the app build) — a mismatch between\na connector key in `AppConnectorsSchema` and the `connectors` option surfaces here —\nand confirm each connector key matches its `name` in `rayfin.yml`.\n\n## Reading & writing connector entities (per dialect)\n\nOnce wired (Step 8), each Category A entity is reached through\n`client.connectors.\u003Cname>.\u003CEntity>`. What the surface exposes depends on two\nthings: the connector's `operations:` (which gate the verbs) and the connector\n**type\u002Fdialect** (which shapes what writes return). This section is the\nBuilder-facing usage reference for the three GraphQL-backed types.\n\n### Reads (all three dialects)\n\nReads work identically on Lakehouse, Warehouse, and SQL Database — the read\nsurface is the same; only writes differ.\n\n```ts\n\u002F\u002F Query chain: select -> where -> orderBy -> execute\nconst orders = await client.connectors.inventory.Order\n  .select(['orderId', 'customerEmail', 'total'])\n  .where({ total: { gt: 100 } })\n  .orderBy({ total: 'desc' })\n  .execute();\n\n\u002F\u002F By-key read. Pass a key OBJECT (all parts of the composite PK) AND a\n\u002F\u002F required scalar-only `select`. The result is Pick\u003CRow, selected> | null —\n\u002F\u002F only the columns you name are fetched and present on the type.\nconst order = await client.connectors.inventory.Order.findByKey(\n  { orderId: 'o-1' },\n  ['orderId', 'customerEmail', 'total'],\n);\norder?.total; \u002F\u002F present and typed; unselected columns are absent, not `undefined`\n\nconst lineItem = await client.connectors.sales.OrderItem.findByKey(\n  { orderId: 'o-1', productId: 'p-9' }, \u002F\u002F both parts of the composite PK required\n  ['orderId', 'productId', 'quantity'],\n);\n```\n\n`findByKey` takes two arguments: the key object and a **required** `select` of\nscalar columns; it returns `Pick\u003CRow, selected> | null`. Relationships cannot be\nselected via `findByKey` — read them through the query chain (`select`\u002F`where`).\nA composite key requires **all** its parts in the key object; omitting one is a\ncompile error (the by-key `where` shape is an exact `Pick` over the declared key\n— see the [composite-PK contract](#2-composite-pk-tables--declare-the-key)).\n\n### Reading related columns (foreign-key relationships)\n\n`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**.\nThe builder expands each path into the nested selection the server expects (`category { ... }`, or an `items { ... }` connection for a `@many` hop) and unwraps the response so the related row(s) sit inline on the result.\n\n```ts\n\u002F\u002F Given the canonical Product entity (§11): @one category, @many orderItems.\nconst products = await client.connectors.inventory.Product\n  .select(['name', 'category.name', 'orderItems.quantity'])\n  .where({ stock: { gt: 0 } })\n  .execute();\n\nproducts[0].category.name;          \u002F\u002F @one  — related row inline\nproducts[0].orderItems[0].quantity; \u002F\u002F @many — related rows inline (unwrapped)\n\n\u002F\u002F Paths nest to arbitrary depth, hopping across entities.\nawait client.connectors.inventory.Product\n  .select(['name', 'orderItems.order.customerEmail'])\n  .execute();\n```\n\n- **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.\n- **Each path is validated segment-by-segment** against the schema, so a wrong hop (`category.nope`) fails to compile; depth is unbounded.\n- **`findByKey` cannot select relationships** — it is scalar-only (above). Read related data through the `select`\u002F`where` query chain.\n\n> **Self-referencing foreign keys are not supported currently.** A relationship\n> that points back at its own entity (an FK from a table to itself) cannot be\n> queried through — do not select a dotted path across a self-relationship.\n\n### Writes — gated by `operations:` and shaped by dialect\n\n`create` \u002F `update` \u002F `delete` exist only when the connector's `operations:`\n(and the entity `@role(...)`) allow them, and what they **return** is fixed by\nthe connector's dialect:\n\n| Connector type | Dialect | `create`\u002F`update`\u002F`delete` return | Notes |\n|---|---|---|---|\n| `fabric-sqlanalytics` (Lakehouse) | — | **not available** | Read-only at the host; only `select`\u002F`findByKey`\u002Fquery chain exist. Writes are a compile error. |\n| `fabric-sqldatabase` (SQL Database) | row-returning | the **full entity row** | Full CRUD; reads the whole row back after the write — **every** column, including server-generated ones (identity, defaults, computed) the caller did not send. Requires the `entities` map in `connectorConfig` (Step 7). |\n| `fabric-warehouse` (Warehouse) | no `OUTPUT` clause | `DbOperationResult { result: string }` | Full CRUD, but DWSQL cannot read the row back, so mutations resolve to a status object, not the row. On success `result` is the string `\"success\"`; a failed write surfaces as a thrown GraphQL error, not a `DbOperationResult`. |\n\n```ts\n\u002F\u002F SQL Database (fabric-sqldatabase) — mutation returns the full row, with every\n\u002F\u002F server-generated column filled in by the database.\nconst created = await client.connectors.orders.Order.create({\n  quantity: 3,          \u002F\u002F supply only the columns you own…\n  unitPrice: 19.99,\n});\ncreated.id;             \u002F\u002F server-assigned identity — returned, though never sent\ncreated.createdUtc;     \u002F\u002F server default (SYSUTCDATETIME()) — returned\ncreated.lineTotal;      \u002F\u002F computed (quantity * unitPrice) — returned\n\n\u002F\u002F Warehouse (fabric-warehouse) — mutation returns DbOperationResult, NOT the row\nconst result = await client.connectors.inventory.Order.update(\n  { orderId: 'o-1' },          \u002F\u002F key object (all composite parts)\n  { total: 250 },              \u002F\u002F partial update\n);\nresult.result;                 \u002F\u002F \"success\" — status string, not the row; a\n                               \u002F\u002F failed write throws a GraphQL error instead\n\n\u002F\u002F Delete by key (both dialects) — full composite key required\nawait client.connectors.sales.OrderItem.delete({ orderId: 'o-1', productId: 'p-9' });\n\n\u002F\u002F Lakehouse (fabric-sqlanalytics) — writes do not exist\nclient.connectors.analytics.Sales.create({ \u002F* ... *\u002F }); \u002F\u002F compile error: read-only\n```\n\n**Server-generated columns.** These are the columns marked `AutoGenerated\u003CT>`\non the entity (see [§4a](#4a-server-generated-columns--autogeneratedt)) —\n`IDENTITY`, any `DEFAULT` (`newid()`, `newsequentialid()`, `sysutcdatetime()`,\na sequence), and computed (`AS (...)`) columns. They are **optional** on\n`create` \u002F `update` and come back populated in the returned row. Most cannot be\nwritten: passing an `IDENTITY` or computed value is **rejected** by the database\n(not silently ignored), and rowversion \u002F temporal columns are server-maintained.\nThe one exception is a plain `DEFAULT` column — omit it to get the default, or\npass a value to override it. Send only the columns the caller owns; treat\nidentity \u002F computed \u002F rowversion \u002F temporal as read-back-only.\n\nBy-key **`update`\u002F`delete`** take the same full key object as `findByKey`; a\nkeyless entity (`primaryKey` omitted or `[]`) exposes **no** `findByKey` \u002F\n`update` \u002F `delete` at all — it is read-only regardless of the connector type,\nbecause there is no key to address a row by (see the\n[composite-PK contract](#2-composite-pk-tables--declare-the-key)).\n\n### Quick decision guide\n\n- **Lakehouse (`fabric-sqlanalytics`)** → reads only. Model entities, `select`,\n  `findByKey`, query chain. No `@role` write actions, no `create`\u002F`update`\u002F`delete`.\n- **SQL Database (`fabric-sqldatabase`)** → full CRUD; mutations hand back the\n  full row (all columns, server-generated ones included), so you can render the\n  created\u002Fupdated record directly.\n- **Warehouse (`fabric-warehouse`)** → full CRUD; mutations hand back a\n  `DbOperationResult`, so re-query with `findByKey`\u002F`select` if you need the\n  persisted values.\n\n## Connector Command Reference (`inspect`, `search`, `invoke`)\n\nFull reference docs for each connector command live in the Rayfin guide. Read\nthem straight off disk from\n`node_modules\u002F@microsoft\u002Frayfin-guide\u002Fassets\u002Fdocs\u002F\u003Cpath below>`, or fetch them\nwith `rayfin docs get --module guide --path '\u003Cpath below>'` — or `get_doc` with\n`module: \"guide\"` via MCP (if already installed):\n\n| Command | What it does | Doc path |\n|---|---|---|\n| `rayfin connector search [query]` | Discover Fabric sources (warehouses, SQL databases, Lakehouses, semantic models, KQL databases) the signed-in identity can add, before you know workspace\u002Fitem IDs. Scope, filtering, `interactive`\u002F`plain`\u002F`--json` output modes. | `cli\u002Fconnectors\u002Fsearch.md` |\n| `rayfin connector add` | Declare a connector in `rayfin.yml`, scaffold `rayfin\u002Fconnectors\u002F\u003Cname>\u002F`, and run schema discovery. Flags, `--operations` scoping, Category B specifics. | `cli\u002Fconnectors\u002Fadd.md` |\n| `rayfin connector inspect` | Run one read-only sample query against a source before writing app code. Selector x query-mode pairs, entity resolution, SQL\u002FDAX validation, error mapping. | `cli\u002Fconnectors\u002Finspect.md` |\n| `rayfin connector invoke \u003Cname> \u003Coperation>` | Run one named operation against a configured connector — the loop for exercising [Category B](#category-b--function-bridge-connectors) connectors. Payload input, transports, token handling, output contract. | `cli\u002Fconnectors\u002Finvoke.md` |\n| — | Category overview, where connector state lives, deployment. | `cli\u002Fconnectors\u002Findex.md` |\n\nKey facts worth knowing without fetching a doc:\n\n- `connector inspect` supports the three Category A SQL types plus\n  `fabric-semanticmodel` (DAX). `kusto` is **not** supported — it errors with\n  `Unsupported connector type: kusto`.\n- `connector invoke` on `fabric-semanticmodel` calls Fabric\u002FPower BI **directly\n  under the developer's identity**, so it works with or without `rayfin up`.\n  Every other type (including `kusto`) POSTs to the deployed item and requires a\n  prior `rayfin up`.\n- `connector invoke` takes exactly one of `--input '\u003Cjson>'` or `--file \u003Cpath>`,\n  and `--file` must resolve inside the project root.\n- `connector invoke` on `fabric-semanticmodel` returns an already-normalised\n  result, because that connector normalises inside its `invoke` middleware. Do\n  not apply `toQueryResult` to it again. The exact shape is owned by the\n  connector package and released with it, so read `cli\u002Fconnectors\u002Finvoke.md`\n  rather than assuming the field names.\n- A resolved `connector invoke` call is **not** automatically a success — a\n  connector that normalises reports failure as `status: 'error'` on the output,\n  while one that returns the raw service envelope reports it as\n  `status: 'Failed'`. The CLI converts either into a non-zero exit.\n\nRead the matching doc before answering detailed questions about flags, output\nshape, or error messages — do not guess.\n\n## Worked Example — Add a Use Case on Specific Entities\n\nConcrete subset path. 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.\"_\n\n- **Scope + narrow (Steps 2–3).** Operations are `read`, `update`; set `operations:` on the `sales` entry to just those.\n- **Generate the subset (Step 4).** 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 — Subset rule, Step 7).\n- **Roles + aggregate (Steps 5, 7).** `@role('authenticated', ['read', 'update'])` on both; aggregate `SalesSchema = GraphQLBackedConnector\u003C{ Order: typeof Order; OrderItem: typeof OrderItem }, typeof connectorConfig>`, with `connectorConfig` declared `as const satisfies ConnectorConfig` and `operations: ['read', 'update']`.\n- **Wire + verify (Step 8).** Add `sales` to `AppConnectorsSchema` and the `connectors` option. Type-check: `client.connectors.sales.Order.create(...)` is now a compile error (only `read`\u002F`update` exist) — the proof the scope took effect. Deploy with `rayfin up`.\n\n## Entity Generation Contract\n\nWhen you generate `rayfin\u002Fconnectors\u002F\u003Cname>\u002F\u003CEntityName>.ts` from\n`metadata.json`, follow this contract end-to-end. This is the source of\ntruth — the CLI itself does not emit entity files.\n\n### 1. File name and class name\n\n- `className = pascalCase(table.tableName)` (e.g. `product_category` →\n  `ProductCategory`, `OrderItem` → `OrderItem`).\n- **Pluralization is allowed, but must be idempotent — never double-pluralize.**\n  The class name and the `@entity` name come from `table.tableName`, transformed to PascalCase.\n  Pluralizing a **singular** table name is fine (`Order` → `Orders`, `Category` → `Categories`).\n  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 (`Orders`, `Categories`, `SalesLineItems`) — do **not** add another plural suffix (`Orders` → `Orderses`, `Categories` → `Categorieses`).\n  Applying pluralization to an already-plural name is a no-op.\n  Whatever name you settle on, the `@entity` name and the `client.connectors.\u003Cname>.\u003CEntity>` access path must stay consistent with it so the mapping holds at query time.\n- File name is `\u003CclassName>.ts`. One file per table; no nesting.\n- **Cross-connector uniqueness — disambiguate only on collision.**\n  GraphQL type names are **global** across every connector in the app: the `@entity` name defaults to the class name, so two connectors that each produce an entity with the same name (e.g. both have a `product` table → `Product`) collide at `rayfin up connector apply` time even when they point at different physical tables.\n  Before finalizing a name, scan every **other** `rayfin\u002Fconnectors\u002F*\u002F` directory (the entity `.ts` files already generated there) **and** the entities you have already generated for the current connector (two schemas in one connector can also share a table name) for a class \u002F `@entity` name that matches the one you are about to write.\n  If — and only if — the base name is already taken, prefix it with the connector's source database name (PascalCased, from `metadata.json` `source`) to disambiguate: `Product` → `SalesDbProduct`.\n  Disambiguation is required even when the colliding entities refer to the same physical source table; entity-name ownership, not source-table identity, determines whether a collision exists.\n  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) as the prefix instead.\n  **Never** rename a name that does not collide, and **never** blanket-prefix every entity — disambiguate only the colliding name(s), leaving all others exactly as Contract §1 derives them.\n  When you rename an entity, the same disambiguated name must be used everywhere it appears: the class name, the file name (`\u003CclassName>.ts`), the `@entity` name, the `TSchema` key and re-export in [Step 7](#step-7--update-the-aggregate-connector-schema), and the `client.connectors.\u003Cname>.\u003CEntity>` access path — keep all five in sync.\n  Surface a one-line note to the user for each rename (`Entity 'Product' from connector '\u003Cother>' already exists; generated as 'SalesDbProduct' to avoid a GraphQL type collision.`).\n\n### 2. Composite-PK tables — declare the key\n\nIf `table.primaryKeyColumns.length > 1`, emit the entity and declare the\ncomposite key on `Source(...)`, listing every key column — as its TypeScript\nproperty name — in `table.primaryKeyColumns` (`ORDINAL_POSITION`) order. Keep\none property per key column and preserve each SQL column name with `column:`\nwhen it differs.\n\n```ts\nexport class OrderItem extends Source({\n  schema: 'dbo',\n  table: 'OrderItem',\n  primaryKey: ['orderId', 'productId'],\n}) {\n  @uuid({ column: 'OrderID' }) orderId!: string;\n  @uuid({ column: 'ProductID' }) productId!: string;\n  @int() quantity!: number;\n}\n```\n\nEvery key part is then required in the by-key methods\n(`findByKey`\u002F`update`\u002F`delete`). Omitting `primaryKey` makes the entity\nkeyless (no by-key methods), so a keyed table must always declare it.\n\n### 3. Primary-key column\n\n- **Declared single PK**: `pkColumnName = table.primaryKeyColumns[0]`.\n- **No PK** (synthetic-PK fallback): pick the first **non-nullable** column\n  in `table.columns`; if every column is nullable, pick the first column.\n  Record the reason (`first non-nullable column` \u002F\n  `all columns are nullable; first column selected`) — you emit the banner\n  comment in step 8.\n- **Empty table** (zero columns): treat as no PK; no synthetic id.\n\nDeclare the PK on `Source({ ..., primaryKey: [...] })`, listing the key's\nTypeScript property name(s). The property name and datatype are up to you —\nany column works — and the on-disk SQL column name is preserved via the\n`column:` option (see step 5). For a composite key, follow section 2.\n\n### 4. SQL type → decorator mapping\n\nLook up `column.dataType` (case-insensitive) in this table:\n\n| SQL type family | Decorator | TS type |\n|---|---|---|\n| `int`, `bigint`, `smallint`, `tinyint` | `@int()` | `number` |\n| `decimal`, `numeric`, `money`, `smallmoney`, `float`, `real` | `@decimal({ precision, scale })` | `number` |\n| `bit` | `@boolean()` | `boolean` |\n| `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset`, `time` | `@date()` | `Date` |\n| `uniqueidentifier` | `@uuid()` | `string` |\n| `varchar`, `nvarchar`, `char`, `nchar`, `text`, `ntext` | `@text()` | `string` |\n| Anything else (`geography`, `hierarchyid`, `xml`, vector, …) | `@text()` + warning | `string` |\n\nFor the fallback case, emit a warning:\n\n> Unknown SQL type `\u003CdataType>` for `\u003Ctable>.\u003Ccolumn>`; falling back to `@text()`.\n\n### 4a. Server-generated columns → `AutoGenerated\u003CT>`\n\n`metadata.json` flags the columns the database fills in. On each column, check\nfor these server-generation markers:\n\n- `identity` — an `IDENTITY(seed, increment)` key.\n- `default` — a column `DEFAULT` (e.g. `newid()`, `newsequentialid()`,\n  `sysutcdatetime()`, `NEXT VALUE FOR \u003Cseq>`, `((0))`).\n- `computed` — a computed column (`AS (\u003Cexpr>)`).\n- `serverManaged` — a column the server maintains with no user-facing\n  expression: `'rowversion'` (a rowversion\u002Ftimestamp column) or\n  `'temporalRowStart'` \u002F `'temporalRowEnd'` (a temporal `GENERATED ALWAYS AS ROW\n  START\u002FEND` period column).\n\nIf **any** of these is present, the column is server-generated: wrap its TS type\nfrom §4 in `AutoGenerated\u003C…>` (`number` → `AutoGenerated\u003Cnumber>`, `Date` →\n`AutoGenerated\u003CDate>`). The decorator and its `column:` option are unchanged;\ndo **not** emit a `default:` option — the marker is type-only and the connector\nnever writes these columns.\n\n`AutoGenerated\u003CT>` makes the column **optional on create\u002Fupdate input** and read\nback as plain `T` in the returned row. Nullability is unchanged: a non-null\nserver-generated column still uses `!:` (the marker wraps the non-null base\ntype); a nullable one uses `?:`.\n\n**Optional on input does not mean \"accepts a value\".** For most server-generated\ncolumns, passing a value is not merely ignored — the database **rejects** the\nwrite:\n\n- `identity` — inserting an explicit value fails (`Cannot insert explicit value\n  for identity column … when IDENTITY_INSERT is OFF`). Never send it.\n- `computed` — a computed column cannot be written; setting it is a server error.\n- `serverManaged` (rowversion \u002F temporal period columns) — server-maintained;\n  writing them is rejected.\n- plain `default` — the one exception: omit it and the server supplies the\n  default; passing a value is accepted and overrides it (not rejected).\n\nSo: omit every `AutoGenerated` column on `create`, and never include\n`identity` \u002F `computed` \u002F `serverManaged` columns on `update`.\n\n```ts\n@int({ column: 'Id' })\nid!: AutoGenerated\u003Cnumber>;         \u002F\u002F IDENTITY — omit on write, server assigns\n\n@uuid({ column: 'PublicId' })\npublicId!: AutoGenerated\u003Cstring>;   \u002F\u002F DEFAULT newid()\n\n@decimal({ optional: true, column: 'LineTotal', precision: 28, scale: 2 })\nlineTotal?: AutoGenerated\u003Cnumber>;  \u002F\u002F computed: AS ([Quantity] * [UnitPrice])\n\n@text({ column: 'RowVer' })\nrowVer!: AutoGenerated\u003Cstring>;     \u002F\u002F rowversion (serverManaged) — read-back-only\n```\n\n### 5. Field-level decorator options\n\nFor every column, the decorator option object is built in this order\n(omit keys you don't need):\n\n1. `optional: true` — if `column.isNullable` is true.\n2. `column: '\u003CcolumnName>'` — when the SQL column name differs from the\n   TS property name (i.e. when `propName !== columnName`). Single-quote\n   the value; escape embedded `'` as `\\\\'`.\n3. **Text only:** `max: \u003CmaxLength>` — when `decorator === 'text'` and\n   `column.maxLength > 0`.\n4. **Decimal only:** `precision: \u003Cp>, scale: \u003Cs>` — when\n   `decorator === 'decimal'` and both `precision` and `scale` are present\n   in metadata.\n5. **Integer:** do **not** emit `min`\u002F`max` based on SQL precision; those\n   are value bounds, not storage capacity. Plain `@int()` is correct.\n\nIf the option object is empty, emit `@text()` rather than `@text({})`.\n\nProperty name itself is always `camelCase(column.columnName)` — the PK is\nnot special-cased or renamed to `id`. Any column name and any datatype can\nbe a primary key.\n\nNullable columns use `?:`; non-nullable use `!:`.\n\n### 6. Global-type shadow rule\n\nIf `pascalCase(table.tableName) === tsType` for some column (e.g. a table\nnamed `Date` with a `datetime2` column), the unqualified `Date` in the\nannotation will resolve to the entity class, not the global. Render the\ntype as `globalThis.Date` (or whatever the global is) in that one field\nannotation only. All other fields keep their bare type.\n\n### 7. Relationships — `@one` and `@many`\n\nForward (`@one`) and reverse (`@many`) relationships are emitted from\nforeign-key metadata.\n\n**`@one` — one per FK on this table:**\n\nFor each entry in `table.foreignKeys`:\n\n- `fieldName = camelCase(singularize(fk.referencedTableName))`.\n- `sourceFields = [camelCase(fk.columnName)]`.\n- `targetFields = [camelCase(fk.referencedColumnName)]` — the referenced\n  table's PK property.\n- Emit:\n\n  ```ts\n  @one(() => \u003CReferencedClass>, { sourceFields: ['\u003Csrc>'], targetFields: ['\u003Ctgt>'] })\n  \u003CfieldName>!: \u003CReferencedClass>;\n  ```\n\nIf `fk.referencedTableSchema.fk.referencedTableName` is not in the table\nindex (FK to an unknown table), skip this relationship and emit a warning:\n\n> Foreign key `\u003Ctable>.\u003Ccolumn>` references unknown table\n> `\u003CrefSchema>.\u003CrefTable>`; relationship skipped.\n\nIn **subset mode** (Step 4), apply the same skip when the referenced table\nexists in `metadata.json` but is **not** in the set you are generating: omit\nthe `@one` and its sibling import, and warn that the relationship was dropped\nbecause the target is out of scope. Never import a `.\u002F\u003CClass>.js` file you\ndid not write.\n\n**`@many` — reverse-direction relationships pointing AT this table:**\n\nBuild a reverse-FK index across all tables: every FK from\n`otherTable.\u003Ccol>` to `\u003CthisTable>.\u003CrefCol>` becomes a `@many` on\n`\u003CthisTable>`.\n\n- `fieldName = camelCase(pluralize(otherTable.tableName))` — `pluralize` is\n  idempotent, so an already-plural table (`Orders`) stays `orders`, never\n  `orderses`.\n- `sourceFields = [camelCase(fk.referencedColumnName)]` — THIS table's PK\n  property.\n- `targetFields = [camelCase(fk.columnName)]` — the OTHER table's FK column.\n- Emit:\n\n  ```ts\n  @many(() => \u003COtherClass>, { sourceFields: ['\u003Csrc>'], targetFields: ['\u003Ctgt>'] })\n  \u003CfieldName>!: \u003COtherClass>[];\n  ```\n\nIn **subset mode**, only emit a `@many` when the other table is also being\ngenerated; otherwise skip it, since a reverse relationship to an ungenerated\nsibling would dangle.\n\n`singularize` \u002F `pluralize` are the same simplified rules the generator\nuses:\n\n- `singularize`: `ies → y` (length > 3); `xes|ses|ches|shes → drop -es`;\n  `\u003Cnon-s>s → drop trailing -s`; else unchanged.\n- `pluralize` (idempotent — never double-pluralizes): if the name is already\n  plural (ends in `s`, `es`, or `ies`), return it unchanged; otherwise\n  `\u003Cnon-vowel>y → -ies`; `x|z|ch|sh → +es`; else `+s`.\n\n### 8. Synthetic-PK banner\n\nWhen the table had no `primaryKeyColumns` and you fell back to a\nsynthetic id (step 3), emit this header **immediately after** the\n`\u002F\u002F @generated` line and **before** the `import`:\n\n```text\n\u002F\u002F\n\u002F\u002F WARNING: No primary key found on this source table.\n\u002F\u002F\n\u002F\u002F Rayfin requires a primary key for GraphQL identity, mutations, and\n\u002F\u002F relationships. The column `\u003CcolumnName>` (\u003Creason>) has been\n\u002F\u002F declared as the primary key as a fallback.\n\u002F\u002F\n\u002F\u002F Risk: if duplicate values exist in this column, queries by key may\n\u002F\u002F return unstable rows and update\u002Fdelete mutations may affect multiple\n\u002F\u002F rows. To remove this fallback, add a PRIMARY KEY constraint on the\n\u002F\u002F source table, then `rayfin connector remove \u003Cname>` and re-add it.\n```\n\n`\u003Creason>` is `first non-nullable column` or\n`all columns are nullable; first column selected`.\n\n### 9. No-FK-metadata note\n\nWhen `table.foreignKeys === undefined` (Lakehouse \u002F no constraints) AND\nno reverse FKs point at this table, also emit one warning to the user:\n\n> No FK metadata available for `\u003CtableName>`; relationships omitted.\n\nThe entity file itself still generates — just without `@one`\u002F`@many`.\n\n### 10. Imports\n\nBuild the import line deterministically:\n\n- Always include `entity` and `Source`.\n- Then append, in this exact order, any decorator names actually used in\n  the file: `boolean`, `date`, `decimal`, `int`, `text`, `uuid`, `one`,\n  `many`. Skip any not used.\n- All from `@microsoft\u002Frayfin-core\u002Fexperimental`.\n- If any column is server-generated (§4a), also add a type-only import:\n  `import type { AutoGenerated } from '@microsoft\u002Frayfin-core\u002Fexperimental';`.\n- For every relationship target that is a **different** class, add a\n  sibling import: `import { \u003CSiblingClass> } from '.\u002F\u003CSiblingClass>.js';`.\n  Sibling imports are alphabetised. Self-references (`@one(() => Foo)`\n  inside `Foo.ts`) get no sibling import.\n- In subset mode, only relationships that survived the §7 subset skip\n  contribute sibling imports — so every `.\u002F\u003CClass>.js` import always points\n  at a file you generated.\n\n### 11. Example (canonical shape)\n\n```ts\n\u002F\u002F @generated — do not edit.\n\nimport { entity, uuid, text, int, date, one, many, Source } from '@microsoft\u002Frayfin-core\u002Fexperimental';\nimport type { AutoGenerated } from '@microsoft\u002Frayfin-core\u002Fexperimental';\nimport { Category } from '.\u002FCategory.js';\nimport { OrderItem } from '.\u002FOrderItem.js';\n\n@entity()\nexport class Product extends Source({ schema: 'dbo', table: 'Product', primaryKey: ['productId'] }) {\n  @uuid({ column: 'ProductID' })\n  productId!: string;\n\n  @text()\n  name!: string;\n\n  @int()\n  stock!: number;\n\n  \u002F\u002F DEFAULT sysutcdatetime() — server-generated (metadata `default`), so\n  \u002F\u002F AutoGenerated: optional on write, read back in the returned row.\n  @date({ column: 'CreatedUtc' })\n  createdUtc!: AutoGenerated\u003CDate>;\n\n  @one(() => Category, { sourceFields: ['categoryId'], targetFields: ['categoryId'] })\n  category!: Category;\n\n  @many(() => OrderItem, { sourceFields: ['productId'], targetFields: ['productId'] })\n  orderItems!: OrderItem[];\n}\n```\n\n## metadata.json Reference\n\nPath: `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json`.\nWritten by `connector add`; never edit by hand. It is regenerated only by\n`rayfin connector remove \u003Cname>` followed by `rayfin connector add ...`.\n\nTop-level: `SchemaMetadata { source, connector, connectionString, discoveredAt, schemas[] }`.\nEach schema entry: `{ schemaName, tables[] }`.\nEach table: `{ tableName, columns[], foreignKeys?, primaryKeyColumns? }`.\nColumns carry `columnName`, `dataType`, `isNullable`, and optional `maxLength` \u002F `precision` \u002F `scale`.\nServer-generated columns additionally carry `identity` (`{ seed, increment }`), `default` (the SQL default expression), `computed` (the `AS (...)` expression), `serverManaged` (`'rowversion'` \u002F `'temporalRowStart'` \u002F `'temporalRowEnd'`), and `datePrecision` — see [§4a](#4a-server-generated-columns--autogeneratedt).\nForeign keys carry `constraintName`, `columnName`, and the `referencedTableSchema` \u002F `referencedTableName` \u002F `referencedColumnName` triple.\n\nFull TypeScript interfaces live in the CLI source at `packages\u002Ftools\u002Fcli\u002Fsrc\u002Fservices\u002Fschema-discovery.ts`.\n\n`primaryKeyColumns` is populated for single-column **and** composite PKs,\nin `ORDINAL_POSITION` (declared key) order.\n\n## Rules & Anti-Patterns\n\n- The `rayfin.yml` `connectors:` block is an **array** of entries with `name` and `type` fields. Never write the legacy map shape.\n- `rayfin connector add` discovers schema only when `workspaceId` and `itemId` are literal strings — `${VAR}` placeholders are rejected.\n- The typed marker for Category A SQL connectors is `GraphQLBackedConnector\u003CTSchema, typeof connectorConfig>` from `@microsoft\u002Frayfin-connector-fabric-graphql`. Use it in the aggregate `schema.ts`; never invent per-type names like `FabricWarehouse` \u002F `FabricSqlAnalytics` (they do not exist), and never leave `schema.ts` as bare re-exports — Step 8's import of `\u003CName>Schema` and `connectorConfig` would fail.\n- In subset mode, list in `\u003CName>Schema`'s `TSchema` **only** entities you actually generated — a `typeof` reference to an ungenerated class dangles.\n- Keep the verb set identical across three places: `connectorConfig.operations` (which the marker reads via `typeof connectorConfig`), the YAML `operations:`, and each entity `@role(...)` action. Narrow YAML first; never widen above the type's catalog allowlist.\n- Policies use the typed `claims` \u002F `item` DSL from `@microsoft\u002Frayfin-core` — never raw SQL or DAB-policy strings like `\"@claims.sub eq @item.owner_id\"`.\n- Default to the **narrowest** operations the user described — do not assume full CRUD for a read-only ask.\n- **Never double-pluralize an entity\u002Fclass name.**\n  Entity names come from `table.tableName`, PascalCased (Contract §1).\n  Pluralizing a singular table name is allowed (`Order` → `Orders`), but an already-plural table (`Orders`, `Categories`) must stay unchanged — never `Orders` → `Orderses`.\n  Pluralization must be a no-op on names that are already plural.\n- **GraphQL type names are global across connectors — disambiguate duplicates, do not blanket-prefix.**\n  Two connectors whose source tables yield the same entity name (e.g. both `Product`) collide at `rayfin up connector apply` time (Contract §1).\n  Before naming an entity, scan the other `rayfin\u002Fconnectors\u002F*\u002F` directories; whenever another connector already owns the name, prefix the source database name (`SalesDbProduct`), even if both entities refer to the same physical source table.\n  Never rename entities that do not collide, and never prefix every entity by default.\n- When the user asks for one entity, read `metadata.json` and filter — do not regenerate every table.\n- Never edit `metadata.json` or `dab-config.json` by hand — both are regenerated.\n- Connectors do not run under `rayfin dev` — test via `rayfin up` and `rayfin up connector apply` against a deployed environment.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| `rayfin up` fails with \"does not support operation\" | YAML `operations:` contains a name not in the type's catalog allowlist | Remove the offending op or change the connector type. |\n| `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. |\n| `rayfin up connector apply` fails with a duplicate \u002F redefined GraphQL type (e.g. `Product`) | Two connectors generated an entity with the same name — GraphQL type names are global regardless of whether the physical source table is shared (Contract §1) | Disambiguate the colliding entity by prefixing its source database name (`SalesDbProduct`); update the class, file name, `@entity` name, `TSchema` key, and `client.connectors.\u003Cname>.\u003CEntity>` path together, then re-apply. |\n| `rayfin connector add` errors with \"literal IDs required\" | `--workspace-id` or `--item-id` was a `${VAR}` placeholder | Pass literal values, or set the var and re-run. |\n| `rayfin connector add` writes the YAML entry but no entity files | Expected — the CLI never emits entity `.ts` files (see [Step 4](#step-4--generate-entity-files-you-not-the-cli)). | Generate them from `metadata.json` per the contract. If `metadata.json` is also missing, schema discovery failed — fix credentials\u002Fconnectivity, then `rayfin connector remove \u003Cname>` + re-add. |\n| `Property '\u003Cname>' does not exist on connectors` | Connector key in `AppConnectorsSchema` does not match the key in the `connectors` option (or was omitted) | Use the same key — the `rayfin.yml` `name` — in all three places. |\n| A CRUD method is missing from autocomplete on a connector | Expected: the `\u003CName>Schema` type narrows methods to the connector's `operations` | Widen the allow-list via `rayfin connector add --operations` (or edit the connector), then re-import. |\n| Import of `ConnectorsRayfinClient` fails to resolve | Imported from the stable `@microsoft\u002Frayfin-client` entry | Import from `@microsoft\u002Frayfin-client\u002Fexperimental`. |\n| `Cannot find module '@microsoft\u002Frayfin-connector-fabric-graphql'` (or `@microsoft\u002Frayfin-connectors`) | The connector packages were never installed — `connector add` does not add them | `npm install @microsoft\u002Frayfin-connector-fabric-graphql @microsoft\u002Frayfin-connectors` (see [Step 7](#step-7--update-the-aggregate-connector-schema)). |\n| Local `rayfin dev` ignores the connector | Day-one limitation — connectors are cloud-only | Exercise via `rayfin up` against a deployed environment. |\n| `connector inspect` fails with \"must be a single statement\" \u002F \"blocked keyword\" \u002F \"must start with SELECT\\|EVALUATE\" | The `--query` file (or `--entity`-built query) isn't read-only, or has more than one statement | Rewrite as a single read-only `SELECT`\u002F`WITH` (SQL) or `EVALUATE` (DAX) statement — `connector inspect` never allows writes. |\n| `connector inspect --entity \u003Cname>` throws \"exists in multiple schemas\" | The unqualified entity name matches tables in more than one schema | Re-run with a schema-qualified `--entity \u003Cschema>.\u003Ctable>` per the error's suggestion. |\n| `connector inspect --query \u003Cpath>` rejects the file | Path resolves outside the project root, or isn't a `.sql`\u002F`.dax` file | Move the file under the project (e.g. `rayfin\u002Fqueries\u002F`) and use a `.sql`\u002F`.dax` extension. |\n| `connector inspect` errors with \"Unsupported connector type\" | Connector type is `kusto` | Not supported by `connector inspect` today; there is no ad-hoc query path for Kusto connectors yet. |\n| `connector search` fails with \"workspace scope is required\" | No `--workspace-id`\u002F`--all-workspaces`, and no deployments found for the current project | Pass `--workspace-id \u003Cid>` or `--all-workspaces`. |\n| `connector search --all-workspaces` fails with \"--type is required\" | `--all-workspaces` needs a server-side `--type` filter to bound the scan | Add `--type \u003Ctype>` (comma-separated for multiple). |\n| `connector search` interactive picker reports \"You don't have the required permission\" | The pre-flight access check on the picked source failed | Pick a different source, or ask a workspace admin for access, then retry. |\n\n## CLI Quick Reference\n\nFull option details for `connector add` are in [Step 1](#step-1--add-the-connector). Flags in `[brackets]` are optional.\n\n```bash\n# Add: --type required; --workspace-id\u002F--item-id required for Fabric types.\nrayfin connector add --type \u003Ctype> --workspace-id \u003Cws> --item-id \u003Citem> \\\n  [--name \u003Cname>] [--operations read,create,update,delete] [--yes] [--verbose]\n\n# Scope operations at add-time (preferred over editing YAML afterwards)\nrayfin connector add --type fabric-warehouse --workspace-id \u003Cws> --item-id \u003Citem> --operations read,update\n\nrayfin connector list [--verbose] [--json]\n\n# Removes the rayfin.yml entry AND the rayfin\u002Fconnectors\u002F\u003Cname>\u002F directory.\n# Re-add after remove to refresh metadata, then regenerate entity files yourself.\nrayfin connector remove \u003Cname> [--yes]\n\nrayfin up                                              # deploy connectors to the cloud\nrayfin up connector apply [--name \u003Cname>] [--verbose] [--json]   # re-apply DAB config\n\n# Inspect (read-only sample query): one selector + one query mode.\n# Full reference: get_doc module \"guide\", path \"cli\u002Fconnectors\u002Finspect.md\"\nrayfin connector inspect --name \u003Cname> --entity \u003Ctable> [--rows \u003Cn>] [--verbose] [--json]\nrayfin connector inspect --name \u003Cname> --query \u003Cpath.sql|path.dax> [--rows \u003Cn>]\nrayfin connector inspect --workspace-id \u003Cws> --item-id \u003Citem> --type \u003Ctype> --entity \u003Ctable>\nrayfin connector inspect --url \u003Cfabric-portal-url> --query \u003Cpath.dax>\n\n# Search (discover connectable sources).\n# Full reference: get_doc module \"guide\", path \"cli\u002Fconnectors\u002Fsearch.md\"\nrayfin connector search [query] --workspace-id \u003Cws-id>\nrayfin connector search --all-workspaces --type \u003Ctype>[,\u003Ctype>...]\nrayfin connector search --workspace-id \u003Cws-id> --json --limit \u003Cn>\n\n# Invoke (run one named operation): exactly one of --input \u002F --file.\n# Full reference: get_doc module \"guide\", path \"cli\u002Fconnectors\u002Finvoke.md\"\nrayfin connector invoke \u003Cname> \u003Coperation> --input '\u003Cjson>' [--verbose] [--json]\nrayfin connector invoke \u003Cname> \u003Coperation> --file \u003Cpath-inside-project>\n```\n","# Rayfin Connectors\n\nBring an existing Fabric SQL data source (Lakehouse, Warehouse, SQL Database) into a Rayfin app as typed, GraphQL-backed entities. Covers the three day-one connector types, the lifecycle commands, generating entity files from a discovered schema, and scoping the surface with YAML `operations:` and `@role(...)`.\n\n## When to Use a Connector\n\nUse a connector when the user wants the app to read or write **existing** SQL data already living in a Fabric workspace (a Lakehouse, Warehouse, or SQL Database).\n\nDo **not** use a connector when the user is creating a brand-new data model owned by the Rayfin app — those entities go under `rayfin\u002Fdata\u002F` and are managed via `rayfin up db apply`, not `rayfin connector add`.\n\n**Routing note:** `@role` and `@entity` also appear in the base `rayfin` skill for owned `rayfin\u002Fdata\u002F` models. This skill applies only when those decorators are used inside `rayfin\u002Fconnectors\u002F` files, or when the context is connector-level RLS policies and entity generation from a Fabric source.\n\nConnectors are cloud-only on day-one.\n`rayfin dev` parses the `connectors:` block but does not wire it; verifying connector behavior requires `rayfin up`.\n\n**Prerequisite — the `connector` command group is feature-flagged.**\n`rayfin connector ...` is only registered when `RAYFIN_FEATURE_FLAGS` contains `connectors`.\nWithout it the commands do not exist and the CLI reports an unknown command.\n\n**Two categories — decide first.**\n\n- **Category A — GraphQL-backed entities.** Types `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase` surface Fabric SQL as typed entities with read\u002Fcreate\u002Fupdate\u002Fdelete operations and `@role` row-level security.\n  Use the [Step 1–8 workflow](#workflow).\n- **Category B — function-bridge operations.** Type `kusto` (Fabric KQL Database) surfaces a platform-owned function as the named operations `executeQuery` (a KQL query) and `executeCommand` (a Kusto management\u002Fcontrol command); type `fabric-semanticmodel` (DAX) surfaces `executeQuery`. Both run as the signed-in user.\n  Do not generate entities, `@role` policies, or `metadata.json` entities.\n  Use the [Category B section](#category-b--function-bridge-connectors) below and skip Steps 4–6.\n\n## Connector Type Matrix\n\n| Type | Display | Operations the type _can_ support | Auth modes | Required config | Notes |\n|---|---|---|---|---|---|\n| `fabric-sqlanalytics` | Lakehouse SQL endpoint | `read` | `delegated`, `application` | `workspaceId`, `itemId` | Read-only at the host. |\n| `fabric-warehouse` | Warehouse | `read`, `create`, `update`, `delete` | `delegated`, `application` | `workspaceId`, `itemId` | Full CRUD. |\n| `fabric-sqldatabase` | SQL Database | `read`, `create`, `update`, `delete` | `delegated`, `application` | `workspaceId`, `itemId` | Full CRUD. |\n| `fabric-semanticmodel` | Fabric semantic model | `executeQuery` | `delegated` | `workspaceId`, `itemId` | Category B function bridge for DAX; adapter version `1`. |\n| `kusto` | Fabric KQL Database | `executeQuery`, `executeCommand` | `delegated` | `workspaceId`, `itemId` | Category B function bridge for KQL queries and management commands; adapter version `1`. |\n\nThe validator rejects any `operations:` entry not in the connector type's allowlist.\nThe Lakehouse read-only restriction is enforced server-side.\n\n## Category B — Function-Bridge Connectors\n\nA Category B connector is a named-operation surface backed by a small, platform-owned function (UDF).\nThe Builder never writes or sees the function code.\nThe Builder declares the connector and calls a typed method; the Fabric app backend injects connector configuration and delegated authentication before forwarding to the function.\nThere are no GraphQL entities, so do not generate entity files, `@role` policies, or `metadata.json` entities for these connector types.\n\n### Add the connector\n\n```bash\nrayfin connector add \\\n  --type kusto \\\n  --workspace-id \u003Cws-id> \\\n  --item-id \u003Ckql-database-item-id> \\\n  --name telemetry\n```\n\n`--item-id` identifies the Fabric item to query: a KQL Database for `kusto`.\nOmit `--name` to derive the connector name from the Fabric item's display name.\nThe CLI verifies the item, writes the `rayfin.yml` entry, and scaffolds `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts`.\nThe Kusto cluster query endpoint and database are resolved server-side from `(workspaceId, itemId)` at call time.\nNever write a cluster URI or database name to YAML, and never send either value from app code.\n\n### Resulting rayfin.yml\n\n```yaml\nconnectors:\n  - name: telemetry\n    type: kusto\n    version: '1'\n    config:\n      workspaceId: \u003Cws-id>\n      itemId: \u003Ckql-database-item-id>\n    auth:\n      type: delegated\n    operations:\n      - name: executeQuery\n      - name: executeCommand\n```\n\nThe `config` block is the single source of truth for what to query and is not sent on the client wire.\nCategory B connectors are pinned to an adapter version.\n`kusto` allows `executeQuery` and `executeCommand`; `fabric-semanticmodel` allows `executeQuery`. In every case `auth.type` must be `delegated`.\nDelegated authentication uses the signed-in user's identity through the on-behalf-of flow.\n\n### Typed schema\n\nThe CLI-generated `rayfin\u002Fconnectors\u002Ftelemetry\u002Fschema.ts` exports both the typed marker and the runtime connector configuration:\n\n```ts\nimport type { ConnectorConfig } from '@microsoft\u002Frayfin-connectors';\nimport type { Kusto } from '@microsoft\u002Frayfin-connector-kusto';\n\nexport type TelemetrySchema = Kusto\u003C'executeQuery' | 'executeCommand'>;\n\nexport const connectorConfig = {\n  connector: 'kusto',\n} as const satisfies ConnectorConfig;\n```\n\nInstall both imported packages as direct dependencies so the generated file resolves under strict package managers:\n\n```bash\nnpm install @microsoft\u002Frayfin-connector-kusto @microsoft\u002Frayfin-connectors\n```\n\n### Wire into the aggregate schema and client\n\nUse the same `AppConnectorsSchema` and `ConnectorsRayfinClient` wiring pattern as Step 8, but do not add entity re-exports or a `GraphQLBackedConnector`.\nImport the generated `\u003CName>Schema` type and `connectorConfig` value, key both maps by the exact `rayfin.yml` connector name, and pass the config through the client's `connectors` option:\n\n```ts\nimport { ConnectorsRayfinClient } from '@microsoft\u002Frayfin-client\u002Fexperimental';\nimport {\n  type TelemetrySchema,\n  connectorConfig as telemetryConfig,\n} from '..\u002F..\u002Frayfin\u002Fconnectors\u002Ftelemetry\u002Fschema';\n\ntype AppConnectorsSchema = {\n  telemetry: TelemetrySchema;\n};\n\nconst client = new ConnectorsRayfinClient\u003C\n  Record\u003Cstring, never>,\n  Record\u003Cstring, never>,\n  AppConnectorsSchema\n>({\n  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,\n  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,\n  connectors: {\n    telemetry: telemetryConfig,\n  },\n});\n```\n\n### Call it from app code\n\nCorrelation ids are not part of the response body (the connector function relays\nthe Kusto bytes untouched), so generate the `clientRequestId` yourself and pass\nthe same value to both `executeQuery` and `toQueryResult`:\n\n```ts\nconst clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`;\n\nconst response = await client.connectors.telemetry.executeQuery({\n  query: 'StormEvents | summarize count() by State | top 10 by count_',\n  clientRequestId,\n});\n```\n\nOptionally normalize the raw Kusto v1 `{ Tables }` document into a discriminated\nresult that preserves every returned Kusto table:\n\n```ts\nimport { toQueryResult } from '@microsoft\u002Frayfin-connector-kusto';\n\nconst result = toQueryResult(response, { clientRequestId });\nif (result.status === 'success') {\n  renderTables(result.tables);\n} else {\n  showError(result.error.code, result.error.message);\n}\n```\n\nSuccessful results contain `tables` plus the `clientRequestId` you passed in\n(empty when you pass none) and an optional `activityId`; correlation is never on\nthe wire.\nEach table contains named typed `columns` and row-major `rows`.\nError results contain `error.message` and an optional `error.code`.\n\n#### Management commands (`kusto`)\n\n`executeCommand` runs a Kusto management (control) command — the command text\nstarts with a leading dot. It returns the same native Kusto v1 `{ Tables }`\ndocument as `executeQuery`, so normalize it with `toQueryResult` the same way,\nand pass a matching `clientRequestId` to correlate end to end:\n\n```ts\nconst clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`;\n\nconst databases = await client.connectors.telemetry.executeCommand({\n  command: '.show databases',\n  clientRequestId,\n});\n\nconst result = toQueryResult(databases, { clientRequestId });\n```\n\n#### Limiting rows (`fabric-semanticmodel`)\n\n`executeQuery` on `fabric-semanticmodel` accepts an optional `resultSetRowCountLimit`. There is no default. Omit it and every row comes back, so ask the user for a bound rather than inventing one.\n\nPrefer it over wrapping the DAX in `TOPN` when the user wants a guard rather than a deliberately ranked subset. Exceeding it fails the query with an `'overflow'` error, so a truncated result announces itself, where a `TOPN` returns a complete-looking partial answer.\n\nRun `rayfin docs search \"resultSetRowCountLimit\"` for version-locked details, since this behavior is owned by the connector package rather than this skill.\n\n### Verify\n\n`rayfin dev` parses `connectors:` but does not wire Category B calls.\nA real `executeQuery` requires `rayfin up`.\n\n## Workflow\n\nThe Step 1–8 workflow below is for **Category A** GraphQL entity connectors.\nFor **Category B** (`kusto` and `fabric-semanticmodel`), use the Category B section above and skip Steps 4–6 for entity files, `@role`, and row-level security.\n\nFor any connector task, walk these steps in order.\nStop and ask the user when the next step needs information you don't already have.\n\n### Step 1 — Add the connector\n\n```bash\nrayfin connector add --type \u003Ctype> --workspace-id \u003Cws-id> --item-id \u003Citem-id> [--name \u003Cname>] [--operations \u003Cops>]\n```\n\nThe CLI verifies the Fabric item, derives a connector `name` from the item's display name (override with `--name`), writes the entry to `rayfin.yml`, scaffolds `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts`, and runs schema discovery.\n\nEither way, `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json` is written so a subset of entities can be generated later.\n\nOptions for `connector add`:\n\n| Flag | Required | Purpose |\n|---|---|---|\n| `--type \u003Ctype>` | yes | One of `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`. |\n| `--workspace-id \u003Cid>` | yes (Fabric) | Fabric workspace ID. Must be a literal — `${VAR}` placeholders are rejected. |\n| `--item-id \u003Cid>` | yes (Fabric) | Fabric item\u002Fartifact ID. Must be a literal. |\n| `--name \u003Cname>` | no | Connector name. Derived from the item display name when omitted. |\n| `--operations \u003Cops>` | no | Comma-separated subset of the type's allowed operations (e.g. `read,update`). Narrows the emitted `operations:` at add-time. Omit for all allowed operations. Each value must be in the type's catalog allowlist. |\n| `-y, --yes` | no | Auto-accept overwrite\u002Fconfirmation prompts (non-interactive). |\n| `-v, --verbose` | no | Verbose diagnostics. |\n\nPrefer `--operations` to scope the connector at add-time instead of hand-editing\nYAML afterwards (see Step 3); the [CLI Quick Reference](#cli-quick-reference) has a worked example.\n\n### Step 2 — Confirm the user's operation scope\n\n`connector add` writes **every** operation the catalog allows for the type.\nBefore doing anything else, confirm what the user actually needs and narrow `rayfin.yml` accordingly.\n\n| User intent | Operations to keep |\n|---|---|\n| Read-only, analytics, dashboard, \"browse\" | `read` |\n| Append-only log or event capture | `read`, `create` |\n| Users edit existing rows but cannot add new ones | `read`, `update` |\n| Full CRUD app | `read`, `create`, `update`, `delete` |\n\nIf the user's intent is ambiguous, ask explicitly — for example: \"Should the app be allowed to create, update, or delete `\u003Centity>`, or only read it?\".\n\nAlso ask whether rows are scoped per-user (e.g. an `owner_id` \u002F `tenant_id` \u002F `created_by` column suggests row-level security — see Step 6).\n\n### Step 3 — Narrow `operations:` in `rayfin.yml`\n\nIf you already scoped the connector with `--operations` at add-time (Step 1),\nthe YAML already lists only those operations — skip ahead. Otherwise, edit the\nconnector entry to list only the operations the user needs.\nOperations are objects, not bare strings:\n\n```yaml\nconnectors:\n  - name: inventory\n    type: fabric-warehouse\n    config:\n      workspaceId: ${WS_ID}\n      itemId: ${ITEM_ID}\n    auth:\n      type: delegated\n    operations:\n      - name: read\n      - name: update     # removed 'create' and 'delete' per user scope\n```\n\nRules:\n\n- You can narrow below the catalog default; you cannot widen above it.\n- There is no `all` meta-operation — list every action explicitly.\n- The host validator (`ConnectorsSettingsValidator`) rejects unknown or duplicate names at `rayfin up` time.\n\n### Step 4 — Generate entity files (you, not the CLI)\n\n`rayfin connector add` writes `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json` and a\nplaceholder `schema.ts`, then stops. The per-table entity `.ts` files are\n**produced by you** by reading `metadata.json` and following the contract in\n[Entity Generation Contract](#entity-generation-contract). The CLI does not\nemit them.\n\nWhat to do, in order, for every connector-add flow:\n\n1. Read `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json` (shape: `SchemaMetadata` —\n   see [metadata.json Reference](#metadatajson-reference)).\n2. Pick the tables in scope:\n   - **Full set** — every table under `schemas[].tables[]`.\n   - **Subset** — only the tables the user named. Filter\n     `schemas[].tables[]` by `tableName` before generating.\n3. For each surviving table, write a file at\n   `rayfin\u002Fconnectors\u002F\u003Cname>\u002F\u003CEntityName>.ts` following the\n   [Entity Generation Contract](#entity-generation-contract) exactly. The\n   contract covers naming (including cross-connector name-collision\n   disambiguation — scan the other `rayfin\u002Fconnectors\u002F*\u002F` directories first,\n   see [§1](#1-file-name-and-class-name)), type mapping, nullable handling,\n   relationships, primary-key declaration (single or composite),\n   synthetic-PK fallback, and import-line composition.\n4. Overwrite the placeholder `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts` with the\n   aggregate per [Step 7](#step-7--update-the-aggregate-connector-schema).\n5. Surface every warning the contract emits (no-FK-metadata notes, unknown SQL\n   types). Don't drop them silently.\n\nWhen the user asks for a refresh (\"regenerate everything\"), re-do the same flow\nagainst the existing `metadata.json` — only re-run `rayfin connector remove`\nand `rayfin connector add` when the _source schema_ changed and you need fresh\nmetadata (see [metadata.json Reference](#metadatajson-reference)).\n\n### Step 5 — Scope `@role(...)` on entities\n\nThe YAML `operations:` controls which actions reach the source.\nThe entity-level `@role(...)` decorator controls which roles can perform which actions on that entity.\n\nThe entity's `actions` list **must be a subset** of the YAML `operations:` for that connector.\nThe host's settings validator does not catch the mismatch today — DAB will fail at `rayfin up connector apply` time.\nAlways narrow YAML first, then add `@role(...)` to entities (full alignment rule: [Step 7](#step-7--update-the-aggregate-connector-schema)).\n\nExample: a `fabric-warehouse` connector with `operations: [read, update]` and an `Order` entity locked to read-only:\n\n```ts\nimport { entity, int, text, decimal, Source } from '@microsoft\u002Frayfin-core\u002Fexperimental';\nimport { role } from '@microsoft\u002Frayfin-core';\n\n@role('authenticated', ['read'])\n@entity()\nexport class Order extends Source({ schema: 'dbo', table: 'Order', primaryKey: ['orderId'] }) {\n  @int({ column: 'OrderID' }) orderId!: number;\n  @text() customerEmail!: string;\n  @decimal({ precision: 18, scale: 2 }) total!: number;\n}\n```\n\nLegal `actions`: `'read' | 'create' | 'update' | 'delete' | '*'`.\nStack multiple `@role(...)` decorators to give different roles different actions on the same entity.\n\n### Step 6 — Add row-level policies (optional)\n\nUse a `policy` callback on `@role(...)` for row-level security.\nPolicies use the typed `claims` \u002F `item` DSL — never raw SQL strings.\n\n```ts\n@role('authenticated', ['read', 'update'], {\n  policy: (claims, item) => claims.sub.eq(item.owner_id),\n})\n@entity()\nexport class Todo extends Source({ schema: 'dbo', table: 'Todo', primaryKey: ['id'] }) {\n  @uuid() id!: string;\n  @uuid() owner_id!: string;\n  @text() body?: string;\n}\n```\n\nDSL surface: `claims.sub | email | role`, `item.\u003CcolumnName>`, `.eq(...)`, `.and(...)`, `.or(...)`.\nUse any claim the same way — e.g. `claims.email.eq(item.user_email)`.\n`RoleDeclarationOptions` also accepts `include` and `exclude` arrays for field-level allow\u002Fblock lists.\n\nWhen to prompt the user: inspect `metadata.json` for columns named `owner_id`, `user_id`, `tenant_id`, `created_by`, etc.\nIf you see one, ask: \"Should `\u003Ccolumn>` restrict rows so each authenticated user only sees their own?\".\n\n### Step 7 — Update the aggregate connector schema\n\n**Install the connector packages first.** `rayfin connector add` scaffolds files but installs nothing, and the aggregate `schema.ts` imports two packages a fresh app does not yet declare. Add both as direct dependencies before writing the file, or the Step 8 type-check fails with `Cannot find module`:\n\n```bash\nnpm install @microsoft\u002Frayfin-connector-fabric-graphql @microsoft\u002Frayfin-connectors\n```\n\n- `@microsoft\u002Frayfin-connector-fabric-graphql` — provides `GraphQLBackedConnector` (not a dependency of `@microsoft\u002Frayfin-client`, so it is always missing on a fresh app).\n- `@microsoft\u002Frayfin-connectors` — provides `ConnectorConfig`. It ships transitively under `@microsoft\u002Frayfin-client`, but declare it directly so strict resolvers (pnpm) do not treat it as a phantom dependency.\n\nMatch the version line already used by `@microsoft\u002Frayfin-client` in the app's `package.json` (both are `1.x`). Even though both imports are `import type`, TypeScript still needs the packages present at compile time.\n\nThe aggregate `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fschema.ts` is what the app imports from.\nThe CLI leaves a placeholder there; overwrite it so it exports **three** things:\n\n1. **Entity re-exports** — re-export every entity class you generated for this connector.\n2. **`\u003CName>Schema` (type)** — a `GraphQLBackedConnector\u003CTSchema, typeof connectorConfig>` marker from `@microsoft\u002Frayfin-connector-fabric-graphql`. `TSchema` maps each in-scope entity name to its class via `typeof`; the second argument is `typeof connectorConfig` (the config value below, declared `as const satisfies ConnectorConfig`), from which the marker derives both the CRUD operation union and the dialect. This is the typed surface that makes `client.connectors.\u003Cname>.\u003CEntity>.select(...)` strongly typed and gates CRUD methods to the declared operations at compile time.\n3. **`connectorConfig` (value)** — a `ConnectorConfig` from `@microsoft\u002Frayfin-connectors` carrying the runtime `connector` type, the same `operations` allow-list, and an **`entities` map** (each in-scope entity class keyed by its entity name). The runtime routes on `connector`, gates CRUD verbs by `operations`, and reads `entities` so `create` \u002F `update` \u002F `delete` can return the **full row** — including server-generated columns the caller never sent (see [Writes](#writes--gated-by-operations-and-shaped-by-dialect)). **Always include `entities`** in a generated connector's config, keyed exactly as in `TSchema` and covering every entity there — it is a required part of the contract: `create` \u002F `update` \u002F `delete` and no-selection reads depend on it to shape and return the full row. Include it for every dialect so the config is complete and correct.\n\n`GraphQLBackedConnector\u003CTSchema, typeof connectorConfig>` **is** the published typed marker for Category A SQL connectors — use it (see Rules for the full marker contract).\n\n**Single source of truth for operations.**\nFor one connector, the `connectorConfig.operations` array here (which the marker reads via `typeof connectorConfig`), the YAML `operations:` (Step 3), and every entity `@role(...)` action (Step 5) must all describe the same verb set.\nNarrow YAML first, then mirror it into the marker, the config, and the decorators.\n\n**Subset rule.**\nIn subset mode (Step 4), include in `TSchema` **only** the entities you actually generated.\nNever reference an entity class you did not generate — the `typeof` lookup and its import would dangle.\n\n```ts\n\u002F\u002F rayfin\u002Fconnectors\u002Finventory\u002Fschema.ts\nimport type { GraphQLBackedConnector } from '@microsoft\u002Frayfin-connector-fabric-graphql';\nimport type { ConnectorConfig } from '@microsoft\u002Frayfin-connectors';\n\nimport { Order } from '.\u002FOrder.js';\nimport { Customer } from '.\u002FCustomer.js';\n\n\u002F\u002F 1. Re-export the generated entities.\nexport { Order } from '.\u002FOrder.js';\nexport { Customer } from '.\u002FCustomer.js';\n\n\u002F\u002F 2. Runtime routing\u002Fgating config. Consumed by the `connectors` client option\n\u002F\u002F    and, via `typeof connectorConfig`, by the marker below. Use `as const\n\u002F\u002F    satisfies` (not a `: ConnectorConfig` annotation) so the `connector` and\n\u002F\u002F    `operations` literals survive — the marker reads them to derive the\n\u002F\u002F    permitted operations and the dialect (which selects the mutation return\n\u002F\u002F    type: `DbOperationResult` for Fabric Warehouse, the entity row otherwise).\n\u002F\u002F    `entities` maps each in-scope entity class by name so create\u002Fupdate\u002Fdelete\n\u002F\u002F    return the full row (SQL Database); include every entity in `TSchema`.\nexport const connectorConfig = {\n  connector: 'fabric-warehouse',\n  operations: ['read', 'update'],\n  entities: { Order, Customer },\n} as const satisfies ConnectorConfig;\n\n\u002F\u002F 3. Typed marker — entity map + the connector config. Consumed by\n\u002F\u002F    AppConnectorsSchema.\nexport type InventorySchema = GraphQLBackedConnector\u003C\n  { Order: typeof Order; Customer: typeof Customer },\n  typeof connectorConfig\n>;\n```\n\nThe `\u003CName>Schema` name is the PascalCase connector name plus `Schema` (connector `inventory` → `InventorySchema`).\nFor a read-only Lakehouse (`fabric-sqlanalytics`), `operations` is `['read']` (reads only; it never writes).\n\n### Step 8 — Wire connectors into the app client (`rayfin-client.ts`)\n\nDeclaring and deploying a connector does not make it callable from app code.\nThe Builder must expose it through a `ConnectorsRayfinClient` so it shows up as\n`client.connectors.\u003Cname>`.\n\n`ConnectorsRayfinClient` is **experimental** — import it only from the\n`@microsoft\u002Frayfin-client\u002Fexperimental` subpath, never the stable\n`@microsoft\u002Frayfin-client` entry.\n\nEach `schema.ts` exports the `\u003CName>Schema` type and the `connectorConfig` value\n(both defined in [Step 7](#step-7--update-the-aggregate-connector-schema)). The\n`\u003CName>Schema` type goes into `AppConnectorsSchema` so `client.connectors.\u003Cname>`\nis typed; the `connectorConfig` value goes into the `connectors` option so the\nruntime knows which client to build.\n\nThe connector **key must be identical** in three places: the `name` in `rayfin.yml`,\nthe property in `AppConnectorsSchema`, and the property in the `connectors` option.\n\nCreate or update `src\u002Flib\u002Frayfin-client.ts`. One import pair per connector (alias the value — every `schema.ts` exports it as `connectorConfig`); map each connector key to its `\u003CName>Schema` type in `AppConnectorsSchema` and to its config value in the `connectors:` option. Each `\u003CName>Schema` already carries the connector's config (via `typeof connectorConfig` in Step 7), so the dialect types the entity mutation returns — Fabric Warehouse mutations return `DbOperationResult`, others the entity row — with nothing extra to wrap here:\n\n```ts\nimport { ConnectorsRayfinClient } from '@microsoft\u002Frayfin-client\u002Fexperimental';\nimport { ProductDbSchema, connectorConfig as productDbConfig } from '..\u002F..\u002Frayfin\u002Fconnectors\u002FproductDb\u002Fschema';\nimport { InventorySchema, connectorConfig as inventoryConfig } from '..\u002F..\u002Frayfin\u002Fconnectors\u002Finventory\u002Fschema';\n\ntype AppConnectorsSchema = {\n  productDb: ProductDbSchema;\n  inventory: InventorySchema;\n};\n\nexport function getRayfinClient() {\n  \u002F\u002F Type params are \u003CDataSchema, FunctionsSchema, ConnectorsSchema> — set the\n  \u002F\u002F unused slots to Record\u003Cstring, never>, or pass real schema types if the app\n  \u002F\u002F also has a data model \u002F functions.\n  return new ConnectorsRayfinClient\u003CRecord\u003Cstring, never>, Record\u003Cstring, never>, AppConnectorsSchema>({\n    baseUrl: import.meta.env.VITE_RAYFIN_API_URL,\n    publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,\n    authStorage: true,\n    connectors: {\n      productDb: productDbConfig,\n      inventory: inventoryConfig,\n    },\n  });\n}\n```\n\nOnce wired, each connector is reached through `client.connectors.\u003Cname>`, which\nexposes entities narrowed to the connector's `operations`:\n\n```ts\nconst created = await getRayfinClient().connectors.productDb.Colors.create({\n  id: crypto.randomUUID(),\n  name: 'Crimson',\n  hexCode: '#DC143C',\n});\n```\n\n**Write results on Fabric Warehouse.** Fabric Warehouse (DWSQL) has no T-SQL\n`OUTPUT` clause, so it cannot read the written row back. `create` \u002F `update` \u002F\n`delete` on a `fabric-warehouse` connector therefore resolve to\n`DbOperationResult { result: string }` instead of the entity row; every other\nwrite dialect (`fabric-sqldatabase`) returns the written row. So `created` above\nis the written row on SQL Database and a `DbOperationResult` status object on a\nwarehouse — `result` is `\"success\"` on a successful write, while a failed write\nthrows a GraphQL error rather than returning a `\"failure\"` result.\n\n**Querying connector entities.** Reach entities through `client.connectors.\u003Cname>.\u003CEntity>`. Reads use the query chain `.select()` → `.where()` → `.orderBy()` → `.execute()`, plus `findByKey` (pass a key object with all composite parts **and** a required scalar-only `select`) and pagination. The connector's `operations:` gate the surface, and the dialect shapes write returns — see [Reading & writing connector entities](#reading--writing-connector-entities-per-dialect) for the full per-dialect matrix. For aggregation, `groupBy`, or year\u002Fquarter\u002Fmonth time-bucketing, use `search_docs(...)` \u002F `rayfin docs`.\n\nVerify: type-check the project (`tsc --noEmit` or the app build) — a mismatch between\na connector key in `AppConnectorsSchema` and the `connectors` option surfaces here —\nand confirm each connector key matches its `name` in `rayfin.yml`.\n\n## Reading & writing connector entities (per dialect)\n\nOnce wired (Step 8), each Category A entity is reached through\n`client.connectors.\u003Cname>.\u003CEntity>`. What the surface exposes depends on two\nthings: the connector's `operations:` (which gate the verbs) and the connector\n**type\u002Fdialect** (which shapes what writes return). This section is the\nBuilder-facing usage reference for the three GraphQL-backed types.\n\n### Reads (all three dialects)\n\nReads work identically on Lakehouse, Warehouse, and SQL Database — the read\nsurface is the same; only writes differ.\n\n```ts\n\u002F\u002F Query chain: select -> where -> orderBy -> execute\nconst orders = await client.connectors.inventory.Order\n  .select(['orderId', 'customerEmail', 'total'])\n  .where({ total: { gt: 100 } })\n  .orderBy({ total: 'desc' })\n  .execute();\n\n\u002F\u002F By-key read. Pass a key OBJECT (all parts of the composite PK) AND a\n\u002F\u002F required scalar-only `select`. The result is Pick\u003CRow, selected> | null —\n\u002F\u002F only the columns you name are fetched and present on the type.\nconst order = await client.connectors.inventory.Order.findByKey(\n  { orderId: 'o-1' },\n  ['orderId', 'customerEmail', 'total'],\n);\norder?.total; \u002F\u002F present and typed; unselected columns are absent, not `undefined`\n\nconst lineItem = await client.connectors.sales.OrderItem.findByKey(\n  { orderId: 'o-1', productId: 'p-9' }, \u002F\u002F both parts of the composite PK required\n  ['orderId', 'productId', 'quantity'],\n);\n```\n\n`findByKey` takes two arguments: the key object and a **required** `select` of\nscalar columns; it returns `Pick\u003CRow, selected> | null`. Relationships cannot be\nselected via `findByKey` — read them through the query chain (`select`\u002F`where`).\nA composite key requires **all** its parts in the key object; omitting one is a\ncompile error (the by-key `where` shape is an exact `Pick` over the declared key\n— see the [composite-PK contract](#2-composite-pk-tables--declare-the-key)).\n\n### Reading related columns (foreign-key relationships)\n\n`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**.\nThe builder expands each path into the nested selection the server expects (`category { ... }`, or an `items { ... }` connection for a `@many` hop) and unwraps the response so the related row(s) sit inline on the result.\n\n```ts\n\u002F\u002F Given the canonical Product entity (§11): @one category, @many orderItems.\nconst products = await client.connectors.inventory.Product\n  .select(['name', 'category.name', 'orderItems.quantity'])\n  .where({ stock: { gt: 0 } })\n  .execute();\n\nproducts[0].category.name;          \u002F\u002F @one  — related row inline\nproducts[0].orderItems[0].quantity; \u002F\u002F @many — related rows inline (unwrapped)\n\n\u002F\u002F Paths nest to arbitrary depth, hopping across entities.\nawait client.connectors.inventory.Product\n  .select(['name', 'orderItems.order.customerEmail'])\n  .execute();\n```\n\n- **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.\n- **Each path is validated segment-by-segment** against the schema, so a wrong hop (`category.nope`) fails to compile; depth is unbounded.\n- **`findByKey` cannot select relationships** — it is scalar-only (above). Read related data through the `select`\u002F`where` query chain.\n\n> **Self-referencing foreign keys are not supported currently.** A relationship\n> that points back at its own entity (an FK from a table to itself) cannot be\n> queried through — do not select a dotted path across a self-relationship.\n\n### Writes — gated by `operations:` and shaped by dialect\n\n`create` \u002F `update` \u002F `delete` exist only when the connector's `operations:`\n(and the entity `@role(...)`) allow them, and what they **return** is fixed by\nthe connector's dialect:\n\n| Connector type | Dialect | `create`\u002F`update`\u002F`delete` return | Notes |\n|---|---|---|---|\n| `fabric-sqlanalytics` (Lakehouse) | — | **not available** | Read-only at the host; only `select`\u002F`findByKey`\u002Fquery chain exist. Writes are a compile error. |\n| `fabric-sqldatabase` (SQL Database) | row-returning | the **full entity row** | Full CRUD; reads the whole row back after the write — **every** column, including server-generated ones (identity, defaults, computed) the caller did not send. Requires the `entities` map in `connectorConfig` (Step 7). |\n| `fabric-warehouse` (Warehouse) | no `OUTPUT` clause | `DbOperationResult { result: string }` | Full CRUD, but DWSQL cannot read the row back, so mutations resolve to a status object, not the row. On success `result` is the string `\"success\"`; a failed write surfaces as a thrown GraphQL error, not a `DbOperationResult`. |\n\n```ts\n\u002F\u002F SQL Database (fabric-sqldatabase) — mutation returns the full row, with every\n\u002F\u002F server-generated column filled in by the database.\nconst created = await client.connectors.orders.Order.create({\n  quantity: 3,          \u002F\u002F supply only the columns you own…\n  unitPrice: 19.99,\n});\ncreated.id;             \u002F\u002F server-assigned identity — returned, though never sent\ncreated.createdUtc;     \u002F\u002F server default (SYSUTCDATETIME()) — returned\ncreated.lineTotal;      \u002F\u002F computed (quantity * unitPrice) — returned\n\n\u002F\u002F Warehouse (fabric-warehouse) — mutation returns DbOperationResult, NOT the row\nconst result = await client.connectors.inventory.Order.update(\n  { orderId: 'o-1' },          \u002F\u002F key object (all composite parts)\n  { total: 250 },              \u002F\u002F partial update\n);\nresult.result;                 \u002F\u002F \"success\" — status string, not the row; a\n                               \u002F\u002F failed write throws a GraphQL error instead\n\n\u002F\u002F Delete by key (both dialects) — full composite key required\nawait client.connectors.sales.OrderItem.delete({ orderId: 'o-1', productId: 'p-9' });\n\n\u002F\u002F Lakehouse (fabric-sqlanalytics) — writes do not exist\nclient.connectors.analytics.Sales.create({ \u002F* ... *\u002F }); \u002F\u002F compile error: read-only\n```\n\n**Server-generated columns.** These are the columns marked `AutoGenerated\u003CT>`\non the entity (see [§4a](#4a-server-generated-columns--autogeneratedt)) —\n`IDENTITY`, any `DEFAULT` (`newid()`, `newsequentialid()`, `sysutcdatetime()`,\na sequence), and computed (`AS (...)`) columns. They are **optional** on\n`create` \u002F `update` and come back populated in the returned row. Most cannot be\nwritten: passing an `IDENTITY` or computed value is **rejected** by the database\n(not silently ignored), and rowversion \u002F temporal columns are server-maintained.\nThe one exception is a plain `DEFAULT` column — omit it to get the default, or\npass a value to override it. Send only the columns the caller owns; treat\nidentity \u002F computed \u002F rowversion \u002F temporal as read-back-only.\n\nBy-key **`update`\u002F`delete`** take the same full key object as `findByKey`; a\nkeyless entity (`primaryKey` omitted or `[]`) exposes **no** `findByKey` \u002F\n`update` \u002F `delete` at all — it is read-only regardless of the connector type,\nbecause there is no key to address a row by (see the\n[composite-PK contract](#2-composite-pk-tables--declare-the-key)).\n\n### Quick decision guide\n\n- **Lakehouse (`fabric-sqlanalytics`)** → reads only. Model entities, `select`,\n  `findByKey`, query chain. No `@role` write actions, no `create`\u002F`update`\u002F`delete`.\n- **SQL Database (`fabric-sqldatabase`)** → full CRUD; mutations hand back the\n  full row (all columns, server-generated ones included), so you can render the\n  created\u002Fupdated record directly.\n- **Warehouse (`fabric-warehouse`)** → full CRUD; mutations hand back a\n  `DbOperationResult`, so re-query with `findByKey`\u002F`select` if you need the\n  persisted values.\n\n## Connector Command Reference (`inspect`, `search`, `invoke`)\n\nFull reference docs for each connector command live in the Rayfin guide. Read\nthem straight off disk from\n`node_modules\u002F@microsoft\u002Frayfin-guide\u002Fassets\u002Fdocs\u002F\u003Cpath below>`, or fetch them\nwith `rayfin docs get --module guide --path '\u003Cpath below>'` — or `get_doc` with\n`module: \"guide\"` via MCP (if already installed):\n\n| Command | What it does | Doc path |\n|---|---|---|\n| `rayfin connector search [query]` | Discover Fabric sources (warehouses, SQL databases, Lakehouses, semantic models, KQL databases) the signed-in identity can add, before you know workspace\u002Fitem IDs. Scope, filtering, `interactive`\u002F`plain`\u002F`--json` output modes. | `cli\u002Fconnectors\u002Fsearch.md` |\n| `rayfin connector add` | Declare a connector in `rayfin.yml`, scaffold `rayfin\u002Fconnectors\u002F\u003Cname>\u002F`, and run schema discovery. Flags, `--operations` scoping, Category B specifics. | `cli\u002Fconnectors\u002Fadd.md` |\n| `rayfin connector inspect` | Run one read-only sample query against a source before writing app code. Selector x query-mode pairs, entity resolution, SQL\u002FDAX validation, error mapping. | `cli\u002Fconnectors\u002Finspect.md` |\n| `rayfin connector invoke \u003Cname> \u003Coperation>` | Run one named operation against a configured connector — the loop for exercising [Category B](#category-b--function-bridge-connectors) connectors. Payload input, transports, token handling, output contract. | `cli\u002Fconnectors\u002Finvoke.md` |\n| — | Category overview, where connector state lives, deployment. | `cli\u002Fconnectors\u002Findex.md` |\n\nKey facts worth knowing without fetching a doc:\n\n- `connector inspect` supports the three Category A SQL types plus\n  `fabric-semanticmodel` (DAX). `kusto` is **not** supported — it errors with\n  `Unsupported connector type: kusto`.\n- `connector invoke` on `fabric-semanticmodel` calls Fabric\u002FPower BI **directly\n  under the developer's identity**, so it works with or without `rayfin up`.\n  Every other type (including `kusto`) POSTs to the deployed item and requires a\n  prior `rayfin up`.\n- `connector invoke` takes exactly one of `--input '\u003Cjson>'` or `--file \u003Cpath>`,\n  and `--file` must resolve inside the project root.\n- `connector invoke` on `fabric-semanticmodel` returns an already-normalised\n  result, because that connector normalises inside its `invoke` middleware. Do\n  not apply `toQueryResult` to it again. The exact shape is owned by the\n  connector package and released with it, so read `cli\u002Fconnectors\u002Finvoke.md`\n  rather than assuming the field names.\n- A resolved `connector invoke` call is **not** automatically a success — a\n  connector that normalises reports failure as `status: 'error'` on the output,\n  while one that returns the raw service envelope reports it as\n  `status: 'Failed'`. The CLI converts either into a non-zero exit.\n\nRead the matching doc before answering detailed questions about flags, output\nshape, or error messages — do not guess.\n\n## Worked Example — Add a Use Case on Specific Entities\n\nConcrete subset path. 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.\"_\n\n- **Scope + narrow (Steps 2–3).** Operations are `read`, `update`; set `operations:` on the `sales` entry to just those.\n- **Generate the subset (Step 4).** 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 — Subset rule, Step 7).\n- **Roles + aggregate (Steps 5, 7).** `@role('authenticated', ['read', 'update'])` on both; aggregate `SalesSchema = GraphQLBackedConnector\u003C{ Order: typeof Order; OrderItem: typeof OrderItem }, typeof connectorConfig>`, with `connectorConfig` declared `as const satisfies ConnectorConfig` and `operations: ['read', 'update']`.\n- **Wire + verify (Step 8).** Add `sales` to `AppConnectorsSchema` and the `connectors` option. Type-check: `client.connectors.sales.Order.create(...)` is now a compile error (only `read`\u002F`update` exist) — the proof the scope took effect. Deploy with `rayfin up`.\n\n## Entity Generation Contract\n\nWhen you generate `rayfin\u002Fconnectors\u002F\u003Cname>\u002F\u003CEntityName>.ts` from\n`metadata.json`, follow this contract end-to-end. This is the source of\ntruth — the CLI itself does not emit entity files.\n\n### 1. File name and class name\n\n- `className = pascalCase(table.tableName)` (e.g. `product_category` →\n  `ProductCategory`, `OrderItem` → `OrderItem`).\n- **Pluralization is allowed, but must be idempotent — never double-pluralize.**\n  The class name and the `@entity` name come from `table.tableName`, transformed to PascalCase.\n  Pluralizing a **singular** table name is fine (`Order` → `Orders`, `Category` → `Categories`).\n  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 (`Orders`, `Categories`, `SalesLineItems`) — do **not** add another plural suffix (`Orders` → `Orderses`, `Categories` → `Categorieses`).\n  Applying pluralization to an already-plural name is a no-op.\n  Whatever name you settle on, the `@entity` name and the `client.connectors.\u003Cname>.\u003CEntity>` access path must stay consistent with it so the mapping holds at query time.\n- File name is `\u003CclassName>.ts`. One file per table; no nesting.\n- **Cross-connector uniqueness — disambiguate only on collision.**\n  GraphQL type names are **global** across every connector in the app: the `@entity` name defaults to the class name, so two connectors that each produce an entity with the same name (e.g. both have a `product` table → `Product`) collide at `rayfin up connector apply` time even when they point at different physical tables.\n  Before finalizing a name, scan every **other** `rayfin\u002Fconnectors\u002F*\u002F` directory (the entity `.ts` files already generated there) **and** the entities you have already generated for the current connector (two schemas in one connector can also share a table name) for a class \u002F `@entity` name that matches the one you are about to write.\n  If — and only if — the base name is already taken, prefix it with the connector's source database name (PascalCased, from `metadata.json` `source`) to disambiguate: `Product` → `SalesDbProduct`.\n  Disambiguation is required even when the colliding entities refer to the same physical source table; entity-name ownership, not source-table identity, determines whether a collision exists.\n  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) as the prefix instead.\n  **Never** rename a name that does not collide, and **never** blanket-prefix every entity — disambiguate only the colliding name(s), leaving all others exactly as Contract §1 derives them.\n  When you rename an entity, the same disambiguated name must be used everywhere it appears: the class name, the file name (`\u003CclassName>.ts`), the `@entity` name, the `TSchema` key and re-export in [Step 7](#step-7--update-the-aggregate-connector-schema), and the `client.connectors.\u003Cname>.\u003CEntity>` access path — keep all five in sync.\n  Surface a one-line note to the user for each rename (`Entity 'Product' from connector '\u003Cother>' already exists; generated as 'SalesDbProduct' to avoid a GraphQL type collision.`).\n\n### 2. Composite-PK tables — declare the key\n\nIf `table.primaryKeyColumns.length > 1`, emit the entity and declare the\ncomposite key on `Source(...)`, listing every key column — as its TypeScript\nproperty name — in `table.primaryKeyColumns` (`ORDINAL_POSITION`) order. Keep\none property per key column and preserve each SQL column name with `column:`\nwhen it differs.\n\n```ts\nexport class OrderItem extends Source({\n  schema: 'dbo',\n  table: 'OrderItem',\n  primaryKey: ['orderId', 'productId'],\n}) {\n  @uuid({ column: 'OrderID' }) orderId!: string;\n  @uuid({ column: 'ProductID' }) productId!: string;\n  @int() quantity!: number;\n}\n```\n\nEvery key part is then required in the by-key methods\n(`findByKey`\u002F`update`\u002F`delete`). Omitting `primaryKey` makes the entity\nkeyless (no by-key methods), so a keyed table must always declare it.\n\n### 3. Primary-key column\n\n- **Declared single PK**: `pkColumnName = table.primaryKeyColumns[0]`.\n- **No PK** (synthetic-PK fallback): pick the first **non-nullable** column\n  in `table.columns`; if every column is nullable, pick the first column.\n  Record the reason (`first non-nullable column` \u002F\n  `all columns are nullable; first column selected`) — you emit the banner\n  comment in step 8.\n- **Empty table** (zero columns): treat as no PK; no synthetic id.\n\nDeclare the PK on `Source({ ..., primaryKey: [...] })`, listing the key's\nTypeScript property name(s). The property name and datatype are up to you —\nany column works — and the on-disk SQL column name is preserved via the\n`column:` option (see step 5). For a composite key, follow section 2.\n\n### 4. SQL type → decorator mapping\n\nLook up `column.dataType` (case-insensitive) in this table:\n\n| SQL type family | Decorator | TS type |\n|---|---|---|\n| `int`, `bigint`, `smallint`, `tinyint` | `@int()` | `number` |\n| `decimal`, `numeric`, `money`, `smallmoney`, `float`, `real` | `@decimal({ precision, scale })` | `number` |\n| `bit` | `@boolean()` | `boolean` |\n| `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset`, `time` | `@date()` | `Date` |\n| `uniqueidentifier` | `@uuid()` | `string` |\n| `varchar`, `nvarchar`, `char`, `nchar`, `text`, `ntext` | `@text()` | `string` |\n| Anything else (`geography`, `hierarchyid`, `xml`, vector, …) | `@text()` + warning | `string` |\n\nFor the fallback case, emit a warning:\n\n> Unknown SQL type `\u003CdataType>` for `\u003Ctable>.\u003Ccolumn>`; falling back to `@text()`.\n\n### 4a. Server-generated columns → `AutoGenerated\u003CT>`\n\n`metadata.json` flags the columns the database fills in. On each column, check\nfor these server-generation markers:\n\n- `identity` — an `IDENTITY(seed, increment)` key.\n- `default` — a column `DEFAULT` (e.g. `newid()`, `newsequentialid()`,\n  `sysutcdatetime()`, `NEXT VALUE FOR \u003Cseq>`, `((0))`).\n- `computed` — a computed column (`AS (\u003Cexpr>)`).\n- `serverManaged` — a column the server maintains with no user-facing\n  expression: `'rowversion'` (a rowversion\u002Ftimestamp column) or\n  `'temporalRowStart'` \u002F `'temporalRowEnd'` (a temporal `GENERATED ALWAYS AS ROW\n  START\u002FEND` period column).\n\nIf **any** of these is present, the column is server-generated: wrap its TS type\nfrom §4 in `AutoGenerated\u003C…>` (`number` → `AutoGenerated\u003Cnumber>`, `Date` →\n`AutoGenerated\u003CDate>`). The decorator and its `column:` option are unchanged;\ndo **not** emit a `default:` option — the marker is type-only and the connector\nnever writes these columns.\n\n`AutoGenerated\u003CT>` makes the column **optional on create\u002Fupdate input** and read\nback as plain `T` in the returned row. Nullability is unchanged: a non-null\nserver-generated column still uses `!:` (the marker wraps the non-null base\ntype); a nullable one uses `?:`.\n\n**Optional on input does not mean \"accepts a value\".** For most server-generated\ncolumns, passing a value is not merely ignored — the database **rejects** the\nwrite:\n\n- `identity` — inserting an explicit value fails (`Cannot insert explicit value\n  for identity column … when IDENTITY_INSERT is OFF`). Never send it.\n- `computed` — a computed column cannot be written; setting it is a server error.\n- `serverManaged` (rowversion \u002F temporal period columns) — server-maintained;\n  writing them is rejected.\n- plain `default` — the one exception: omit it and the server supplies the\n  default; passing a value is accepted and overrides it (not rejected).\n\nSo: omit every `AutoGenerated` column on `create`, and never include\n`identity` \u002F `computed` \u002F `serverManaged` columns on `update`.\n\n```ts\n@int({ column: 'Id' })\nid!: AutoGenerated\u003Cnumber>;         \u002F\u002F IDENTITY — omit on write, server assigns\n\n@uuid({ column: 'PublicId' })\npublicId!: AutoGenerated\u003Cstring>;   \u002F\u002F DEFAULT newid()\n\n@decimal({ optional: true, column: 'LineTotal', precision: 28, scale: 2 })\nlineTotal?: AutoGenerated\u003Cnumber>;  \u002F\u002F computed: AS ([Quantity] * [UnitPrice])\n\n@text({ column: 'RowVer' })\nrowVer!: AutoGenerated\u003Cstring>;     \u002F\u002F rowversion (serverManaged) — read-back-only\n```\n\n### 5. Field-level decorator options\n\nFor every column, the decorator option object is built in this order\n(omit keys you don't need):\n\n1. `optional: true` — if `column.isNullable` is true.\n2. `column: '\u003CcolumnName>'` — when the SQL column name differs from the\n   TS property name (i.e. when `propName !== columnName`). Single-quote\n   the value; escape embedded `'` as `\\\\'`.\n3. **Text only:** `max: \u003CmaxLength>` — when `decorator === 'text'` and\n   `column.maxLength > 0`.\n4. **Decimal only:** `precision: \u003Cp>, scale: \u003Cs>` — when\n   `decorator === 'decimal'` and both `precision` and `scale` are present\n   in metadata.\n5. **Integer:** do **not** emit `min`\u002F`max` based on SQL precision; those\n   are value bounds, not storage capacity. Plain `@int()` is correct.\n\nIf the option object is empty, emit `@text()` rather than `@text({})`.\n\nProperty name itself is always `camelCase(column.columnName)` — the PK is\nnot special-cased or renamed to `id`. Any column name and any datatype can\nbe a primary key.\n\nNullable columns use `?:`; non-nullable use `!:`.\n\n### 6. Global-type shadow rule\n\nIf `pascalCase(table.tableName) === tsType` for some column (e.g. a table\nnamed `Date` with a `datetime2` column), the unqualified `Date` in the\nannotation will resolve to the entity class, not the global. Render the\ntype as `globalThis.Date` (or whatever the global is) in that one field\nannotation only. All other fields keep their bare type.\n\n### 7. Relationships — `@one` and `@many`\n\nForward (`@one`) and reverse (`@many`) relationships are emitted from\nforeign-key metadata.\n\n**`@one` — one per FK on this table:**\n\nFor each entry in `table.foreignKeys`:\n\n- `fieldName = camelCase(singularize(fk.referencedTableName))`.\n- `sourceFields = [camelCase(fk.columnName)]`.\n- `targetFields = [camelCase(fk.referencedColumnName)]` — the referenced\n  table's PK property.\n- Emit:\n\n  ```ts\n  @one(() => \u003CReferencedClass>, { sourceFields: ['\u003Csrc>'], targetFields: ['\u003Ctgt>'] })\n  \u003CfieldName>!: \u003CReferencedClass>;\n  ```\n\nIf `fk.referencedTableSchema.fk.referencedTableName` is not in the table\nindex (FK to an unknown table), skip this relationship and emit a warning:\n\n> Foreign key `\u003Ctable>.\u003Ccolumn>` references unknown table\n> `\u003CrefSchema>.\u003CrefTable>`; relationship skipped.\n\nIn **subset mode** (Step 4), apply the same skip when the referenced table\nexists in `metadata.json` but is **not** in the set you are generating: omit\nthe `@one` and its sibling import, and warn that the relationship was dropped\nbecause the target is out of scope. Never import a `.\u002F\u003CClass>.js` file you\ndid not write.\n\n**`@many` — reverse-direction relationships pointing AT this table:**\n\nBuild a reverse-FK index across all tables: every FK from\n`otherTable.\u003Ccol>` to `\u003CthisTable>.\u003CrefCol>` becomes a `@many` on\n`\u003CthisTable>`.\n\n- `fieldName = camelCase(pluralize(otherTable.tableName))` — `pluralize` is\n  idempotent, so an already-plural table (`Orders`) stays `orders`, never\n  `orderses`.\n- `sourceFields = [camelCase(fk.referencedColumnName)]` — THIS table's PK\n  property.\n- `targetFields = [camelCase(fk.columnName)]` — the OTHER table's FK column.\n- Emit:\n\n  ```ts\n  @many(() => \u003COtherClass>, { sourceFields: ['\u003Csrc>'], targetFields: ['\u003Ctgt>'] })\n  \u003CfieldName>!: \u003COtherClass>[];\n  ```\n\nIn **subset mode**, only emit a `@many` when the other table is also being\ngenerated; otherwise skip it, since a reverse relationship to an ungenerated\nsibling would dangle.\n\n`singularize` \u002F `pluralize` are the same simplified rules the generator\nuses:\n\n- `singularize`: `ies → y` (length > 3); `xes|ses|ches|shes → drop -es`;\n  `\u003Cnon-s>s → drop trailing -s`; else unchanged.\n- `pluralize` (idempotent — never double-pluralizes): if the name is already\n  plural (ends in `s`, `es`, or `ies`), return it unchanged; otherwise\n  `\u003Cnon-vowel>y → -ies`; `x|z|ch|sh → +es`; else `+s`.\n\n### 8. Synthetic-PK banner\n\nWhen the table had no `primaryKeyColumns` and you fell back to a\nsynthetic id (step 3), emit this header **immediately after** the\n`\u002F\u002F @generated` line and **before** the `import`:\n\n```text\n\u002F\u002F\n\u002F\u002F WARNING: No primary key found on this source table.\n\u002F\u002F\n\u002F\u002F Rayfin requires a primary key for GraphQL identity, mutations, and\n\u002F\u002F relationships. The column `\u003CcolumnName>` (\u003Creason>) has been\n\u002F\u002F declared as the primary key as a fallback.\n\u002F\u002F\n\u002F\u002F Risk: if duplicate values exist in this column, queries by key may\n\u002F\u002F return unstable rows and update\u002Fdelete mutations may affect multiple\n\u002F\u002F rows. To remove this fallback, add a PRIMARY KEY constraint on the\n\u002F\u002F source table, then `rayfin connector remove \u003Cname>` and re-add it.\n```\n\n`\u003Creason>` is `first non-nullable column` or\n`all columns are nullable; first column selected`.\n\n### 9. No-FK-metadata note\n\nWhen `table.foreignKeys === undefined` (Lakehouse \u002F no constraints) AND\nno reverse FKs point at this table, also emit one warning to the user:\n\n> No FK metadata available for `\u003CtableName>`; relationships omitted.\n\nThe entity file itself still generates — just without `@one`\u002F`@many`.\n\n### 10. Imports\n\nBuild the import line deterministically:\n\n- Always include `entity` and `Source`.\n- Then append, in this exact order, any decorator names actually used in\n  the file: `boolean`, `date`, `decimal`, `int`, `text`, `uuid`, `one`,\n  `many`. Skip any not used.\n- All from `@microsoft\u002Frayfin-core\u002Fexperimental`.\n- If any column is server-generated (§4a), also add a type-only import:\n  `import type { AutoGenerated } from '@microsoft\u002Frayfin-core\u002Fexperimental';`.\n- For every relationship target that is a **different** class, add a\n  sibling import: `import { \u003CSiblingClass> } from '.\u002F\u003CSiblingClass>.js';`.\n  Sibling imports are alphabetised. Self-references (`@one(() => Foo)`\n  inside `Foo.ts`) get no sibling import.\n- In subset mode, only relationships that survived the §7 subset skip\n  contribute sibling imports — so every `.\u002F\u003CClass>.js` import always points\n  at a file you generated.\n\n### 11. Example (canonical shape)\n\n```ts\n\u002F\u002F @generated — do not edit.\n\nimport { entity, uuid, text, int, date, one, many, Source } from '@microsoft\u002Frayfin-core\u002Fexperimental';\nimport type { AutoGenerated } from '@microsoft\u002Frayfin-core\u002Fexperimental';\nimport { Category } from '.\u002FCategory.js';\nimport { OrderItem } from '.\u002FOrderItem.js';\n\n@entity()\nexport class Product extends Source({ schema: 'dbo', table: 'Product', primaryKey: ['productId'] }) {\n  @uuid({ column: 'ProductID' })\n  productId!: string;\n\n  @text()\n  name!: string;\n\n  @int()\n  stock!: number;\n\n  \u002F\u002F DEFAULT sysutcdatetime() — server-generated (metadata `default`), so\n  \u002F\u002F AutoGenerated: optional on write, read back in the returned row.\n  @date({ column: 'CreatedUtc' })\n  createdUtc!: AutoGenerated\u003CDate>;\n\n  @one(() => Category, { sourceFields: ['categoryId'], targetFields: ['categoryId'] })\n  category!: Category;\n\n  @many(() => OrderItem, { sourceFields: ['productId'], targetFields: ['productId'] })\n  orderItems!: OrderItem[];\n}\n```\n\n## metadata.json Reference\n\nPath: `rayfin\u002Fconnectors\u002F\u003Cname>\u002Fmetadata.json`.\nWritten by `connector add`; never edit by hand. It is regenerated only by\n`rayfin connector remove \u003Cname>` followed by `rayfin connector add ...`.\n\nTop-level: `SchemaMetadata { source, connector, connectionString, discoveredAt, schemas[] }`.\nEach schema entry: `{ schemaName, tables[] }`.\nEach table: `{ tableName, columns[], foreignKeys?, primaryKeyColumns? }`.\nColumns carry `columnName`, `dataType`, `isNullable`, and optional `maxLength` \u002F `precision` \u002F `scale`.\nServer-generated columns additionally carry `identity` (`{ seed, increment }`), `default` (the SQL default expression), `computed` (the `AS (...)` expression), `serverManaged` (`'rowversion'` \u002F `'temporalRowStart'` \u002F `'temporalRowEnd'`), and `datePrecision` — see [§4a](#4a-server-generated-columns--autogeneratedt).\nForeign keys carry `constraintName`, `columnName`, and the `referencedTableSchema` \u002F `referencedTableName` \u002F `referencedColumnName` triple.\n\nFull TypeScript interfaces live in the CLI source at `packages\u002Ftools\u002Fcli\u002Fsrc\u002Fservices\u002Fschema-discovery.ts`.\n\n`primaryKeyColumns` is populated for single-column **and** composite PKs,\nin `ORDINAL_POSITION` (declared key) order.\n\n## Rules & Anti-Patterns\n\n- The `rayfin.yml` `connectors:` block is an **array** of entries with `name` and `type` fields. Never write the legacy map shape.\n- `rayfin connector add` discovers schema only when `workspaceId` and `itemId` are literal strings — `${VAR}` placeholders are rejected.\n- The typed marker for Category A SQL connectors is `GraphQLBackedConnector\u003CTSchema, typeof connectorConfig>` from `@microsoft\u002Frayfin-connector-fabric-graphql`. Use it in the aggregate `schema.ts`; never invent per-type names like `FabricWarehouse` \u002F `FabricSqlAnalytics` (they do not exist), and never leave `schema.ts` as bare re-exports — Step 8's import of `\u003CName>Schema` and `connectorConfig` would fail.\n- In subset mode, list in `\u003CName>Schema`'s `TSchema` **only** entities you actually generated — a `typeof` reference to an ungenerated class dangles.\n- Keep the verb set identical across three places: `connectorConfig.operations` (which the marker reads via `typeof connectorConfig`), the YAML `operations:`, and each entity `@role(...)` action. Narrow YAML first; never widen above the type's catalog allowlist.\n- Policies use the typed `claims` \u002F `item` DSL from `@microsoft\u002Frayfin-core` — never raw SQL or DAB-policy strings like `\"@claims.sub eq @item.owner_id\"`.\n- Default to the **narrowest** operations the user described — do not assume full CRUD for a read-only ask.\n- **Never double-pluralize an entity\u002Fclass name.**\n  Entity names come from `table.tableName`, PascalCased (Contract §1).\n  Pluralizing a singular table name is allowed (`Order` → `Orders`), but an already-plural table (`Orders`, `Categories`) must stay unchanged — never `Orders` → `Orderses`.\n  Pluralization must be a no-op on names that are already plural.\n- **GraphQL type names are global across connectors — disambiguate duplicates, do not blanket-prefix.**\n  Two connectors whose source tables yield the same entity name (e.g. both `Product`) collide at `rayfin up connector apply` time (Contract §1).\n  Before naming an entity, scan the other `rayfin\u002Fconnectors\u002F*\u002F` directories; whenever another connector already owns the name, prefix the source database name (`SalesDbProduct`), even if both entities refer to the same physical source table.\n  Never rename entities that do not collide, and never prefix every entity by default.\n- When the user asks for one entity, read `metadata.json` and filter — do not regenerate every table.\n- Never edit `metadata.json` or `dab-config.json` by hand — both are regenerated.\n- Connectors do not run under `rayfin dev` — test via `rayfin up` and `rayfin up connector apply` against a deployed environment.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| `rayfin up` fails with \"does not support operation\" | YAML `operations:` contains a name not in the type's catalog allowlist | Remove the offending op or change the connector type. |\n| `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. |\n| `rayfin up connector apply` fails with a duplicate \u002F redefined GraphQL type (e.g. `Product`) | Two connectors generated an entity with the same name — GraphQL type names are global regardless of whether the physical source table is shared (Contract §1) | Disambiguate the colliding entity by prefixing its source database name (`SalesDbProduct`); update the class, file name, `@entity` name, `TSchema` key, and `client.connectors.\u003Cname>.\u003CEntity>` path together, then re-apply. |\n| `rayfin connector add` errors with \"literal IDs required\" | `--workspace-id` or `--item-id` was a `${VAR}` placeholder | Pass literal values, or set the var and re-run. |\n| `rayfin connector add` writes the YAML entry but no entity files | Expected — the CLI never emits entity `.ts` files (see [Step 4](#step-4--generate-entity-files-you-not-the-cli)). | Generate them from `metadata.json` per the contract. If `metadata.json` is also missing, schema discovery failed — fix credentials\u002Fconnectivity, then `rayfin connector remove \u003Cname>` + re-add. |\n| `Property '\u003Cname>' does not exist on connectors` | Connector key in `AppConnectorsSchema` does not match the key in the `connectors` option (or was omitted) | Use the same key — the `rayfin.yml` `name` — in all three places. |\n| A CRUD method is missing from autocomplete on a connector | Expected: the `\u003CName>Schema` type narrows methods to the connector's `operations` | Widen the allow-list via `rayfin connector add --operations` (or edit the connector), then re-import. |\n| Import of `ConnectorsRayfinClient` fails to resolve | Imported from the stable `@microsoft\u002Frayfin-client` entry | Import from `@microsoft\u002Frayfin-client\u002Fexperimental`. |\n| `Cannot find module '@microsoft\u002Frayfin-connector-fabric-graphql'` (or `@microsoft\u002Frayfin-connectors`) | The connector packages were never installed — `connector add` does not add them | `npm install @microsoft\u002Frayfin-connector-fabric-graphql @microsoft\u002Frayfin-connectors` (see [Step 7](#step-7--update-the-aggregate-connector-schema)). |\n| Local `rayfin dev` ignores the connector | Day-one limitation — connectors are cloud-only | Exercise via `rayfin up` against a deployed environment. |\n| `connector inspect` fails with \"must be a single statement\" \u002F \"blocked keyword\" \u002F \"must start with SELECT\\|EVALUATE\" | The `--query` file (or `--entity`-built query) isn't read-only, or has more than one statement | Rewrite as a single read-only `SELECT`\u002F`WITH` (SQL) or `EVALUATE` (DAX) statement — `connector inspect` never allows writes. |\n| `connector inspect --entity \u003Cname>` throws \"exists in multiple schemas\" | The unqualified entity name matches tables in more than one schema | Re-run with a schema-qualified `--entity \u003Cschema>.\u003Ctable>` per the error's suggestion. |\n| `connector inspect --query \u003Cpath>` rejects the file | Path resolves outside the project root, or isn't a `.sql`\u002F`.dax` file | Move the file under the project (e.g. `rayfin\u002Fqueries\u002F`) and use a `.sql`\u002F`.dax` extension. |\n| `connector inspect` errors with \"Unsupported connector type\" | Connector type is `kusto` | Not supported by `connector inspect` today; there is no ad-hoc query path for Kusto connectors yet. |\n| `connector search` fails with \"workspace scope is required\" | No `--workspace-id`\u002F`--all-workspaces`, and no deployments found for the current project | Pass `--workspace-id \u003Cid>` or `--all-workspaces`. |\n| `connector search --all-workspaces` fails with \"--type is required\" | `--all-workspaces` needs a server-side `--type` filter to bound the scan | Add `--type \u003Ctype>` (comma-separated for multiple). |\n| `connector search` interactive picker reports \"You don't have the required permission\" | The pre-flight access check on the picked source failed | Pick a different source, or ask a workspace admin for access, then retry. |\n\n## CLI Quick Reference\n\nFull option details for `connector add` are in [Step 1](#step-1--add-the-connector). Flags in `[brackets]` are optional.\n\n```bash\n# Add: --type required; --workspace-id\u002F--item-id required for Fabric types.\nrayfin connector add --type \u003Ctype> --workspace-id \u003Cws> --item-id \u003Citem> \\\n  [--name \u003Cname>] [--operations read,create,update,delete] [--yes] [--verbose]\n\n# Scope operations at add-time (preferred over editing YAML afterwards)\nrayfin connector add --type fabric-warehouse --workspace-id \u003Cws> --item-id \u003Citem> --operations read,update\n\nrayfin connector list [--verbose] [--json]\n\n# Removes the rayfin.yml entry AND the rayfin\u002Fconnectors\u002F\u003Cname>\u002F directory.\n# Re-add after remove to refresh metadata, then regenerate entity files yourself.\nrayfin connector remove \u003Cname> [--yes]\n\nrayfin up                                              # deploy connectors to the cloud\nrayfin up connector apply [--name \u003Cname>] [--verbose] [--json]   # re-apply DAB config\n\n# Inspect (read-only sample query): one selector + one query mode.\n# Full reference: get_doc module \"guide\", path \"cli\u002Fconnectors\u002Finspect.md\"\nrayfin connector inspect --name \u003Cname> --entity \u003Ctable> [--rows \u003Cn>] [--verbose] [--json]\nrayfin connector inspect --name \u003Cname> --query \u003Cpath.sql|path.dax> [--rows \u003Cn>]\nrayfin connector inspect --workspace-id \u003Cws> --item-id \u003Citem> --type \u003Ctype> --entity \u003Ctable>\nrayfin connector inspect --url \u003Cfabric-portal-url> --query \u003Cpath.dax>\n\n# Search (discover connectable sources).\n# Full reference: get_doc module \"guide\", path \"cli\u002Fconnectors\u002Fsearch.md\"\nrayfin connector search [query] --workspace-id \u003Cws-id>\nrayfin connector search --all-workspaces --type \u003Ctype>[,\u003Ctype>...]\nrayfin connector search --workspace-id \u003Cws-id> --json --limit \u003Cn>\n\n# Invoke (run one named operation): exactly one of --input \u002F --file.\n# Full reference: get_doc module \"guide\", path \"cli\u002Fconnectors\u002Finvoke.md\"\nrayfin connector invoke \u003Cname> \u003Coperation> --input '\u003Cjson>' [--verbose] [--json]\nrayfin connector invoke \u003Cname> \u003Coperation> --file \u003Cpath-inside-project>\n```\n",{"name":5,"metadata":54,"description":7,"rayfin-managed":56},{"author":9,"version":55},"0.5.1",true,"main","d1892a8ab351080f1736d97d8d5fe7fc9e438ed7","templates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Frayfin-connectors",70,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-13 21:12:07",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":56},[68,71,74],{"tool":69,"command":70},"npx","npx skills add microsoft\u002Fawesome-rayfin --skill rayfin-connectors",{"tool":72,"command":73},"bunx","bunx skills add microsoft\u002Fawesome-rayfin --skill rayfin-connectors",{"tool":75,"command":76},"pnpm","pnpm dlx skills add microsoft\u002Fawesome-rayfin --skill rayfin-connectors","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Frayfin-connectors\u002FSKILL.md",[],[80,105,131,154,185,209],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":49},"01M70HMXK23DA6CE0T9R4DNDA9","visuals","Use when user wants to incorporate charts, graphs, data grid,  or other visual representations of data into their project. Use VegaVisual and DataGrid components to create these visuals,  utilizing the shared DataTable input, formatting, theme, and interactivity. Covers the onInteraction selection\u002Fclick events host apps consume, named multi-table data input for layered overlays and reference lines, and Vega-Lite native selections.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Fvisuals\u002FSKILL.md",[],[82,87,88,89,90,32,91,92,93,94,95,96,97,98,99,100,101,102,103,104],"shared","click","theme","Charts","overlays","project","selection","host","use","apps","when","visual","components","grid","multi","create","layered","native",{"skillNo":106,"name":107,"nameZh":6,"description":108,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":109,"categories":110,"keywords":111,"publishedAt":49},"01M70HMVE4BANY7VVP0JR2GQ92","schema-discovery","Explore Power BI semantic model schemas using DAX INFO functions. Progressive metadata discovery strategy, scope estimation, and narrowing techniques for tables, columns, measures, and relationships.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Fschema-discovery\u002FSKILL.md",[],[112,113,114,115,116,117,32,118,119,120,121,122,123,124,125,126,127,128,129,130],"discovery","schema discovery","schema","power","scope","model","explore","techniques","strategy","estimation","power bi","functions","info","bi","relationships","columns","dax","tables","semantic",{"skillNo":132,"name":29,"nameZh":6,"description":133,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":134,"categories":135,"keywords":136,"publishedAt":49},"01M70HMQD0PKCKWCY2NQ8VCCYH","Use when doing ANY task involving Rayfin — scaffolding, data models, decorators, auth, deployment, CLI commands, or client setup. Triggers: rayfin, rayfin init, rayfin up, rayfin login, RayfinClient, @entity, @role, @anonymous, @authenticated, @text, @uuid, @int, @decimal, @boolean, @date, @email, @set, @one, @many, DAB, Data API Builder, rayfin.yml, publishableKey, signUp, signIn, signOut, sendMagicLink, handleMagicLinkCallback, ensureSignedInWithFabric, Fabric SSO, Entra ID, rayfin up db apply, rayfin up staticapp deploy, schema.ts, OpaqueSession, onSessionChange, GraphQL select, GraphQL create, GraphQL update, GraphQL delete, findById, executePaginated, RLS policy, row-level security, claims.sub, claims.email, TC39 decorators, config generation, dialect, mssql, postgresql","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Frayfin\u002FSKILL.md",[],[29,137,138,139,140,141,142,143,144,145,146,147,148,97,149,150,151,152,153,102],"builder","SET","decorators","anonymous","auth","db","scaffolding","select","up","init","entra id","api builder","rls","api","text","mssql","postgresql",{"skillNo":155,"name":156,"nameZh":6,"description":157,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":158,"categories":159,"keywords":169,"publishedAt":49},"01M70HMD71TMHM9NEZV0870V9B","query-design","Separates DAX data-fetching from TypeScript presentation. Guides when to use  DAX vs. TypeScript vs. Vega-Lite for aggregation, total rows, FORMAT(), SELECTCOLUMNS,  BLANK handling, filtering, multi-grain queries, cross-filtering and cross-highlight subset\u002Foverlay queries, and format strings.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Fquery-design\u002FSKILL.md",[160,163,166],{"categoryNo":161,"categoryName":162,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":164,"categoryName":165,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":167,"categoryName":168,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000032","设计师",[170,171,172,173,32,174,175,95,97,176,101,177,178,179,180,181,182,183,184,128],"query design","design","query","overlay","strings","filtering","guides","queries","grain","aggregation","format","typescript","data-fetching","data","when to use",{"skillNo":186,"name":187,"nameZh":6,"description":188,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":189,"categories":190,"keywords":194,"publishedAt":49},"01M70HMAX3MRJWF62P8PAYF1R3","playwright-cli","Automate browser interactions, test web pages and work with Playwright tests.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Fplaywright-cli\u002FSKILL.md",[191],{"categoryNo":192,"categoryName":193,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",[195,196,197,187,198,199,69,200,201,202,203,204,205,206,207,208],"playwright cli","playwright","cli","pages","tests","Bash","browser","with","test","work","web","test web","npm","automate",{"skillNo":210,"name":211,"nameZh":6,"description":212,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":213,"categories":214,"keywords":215,"publishedAt":49},"01M70HM930EA8MQZEKMQR5MFC4","fabric-sdk","How to use @microsoft\u002Ffabric-app-data to connect web applications to Fabric data sources at runtime. Build browser-based apps that query semantic models via DAX using the FabricClient API.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fawesome-rayfin\u002Fblob\u002Fmain\u002Ftemplates\u002Fcaldova-reorder\u002F.agents\u002Fskills\u002Ffabric-sdk\u002FSKILL.md",[],[216,217,218,48,219,220,221,96,172,150,201,222,223,40,224,183,225,226,95,227],"fabric sdk","sdk","fabric","sources","how to","connect","fabric data","how","at","runtime","applications","semantic models"]