Skills로 돌아가기
firebase/agent-skills실행 전 동작 확인

SKILL DETAIL

firebase-data-connect

firebase/agent-skills/firebase-data-connect

Firebase SQL Connect (formerly Firebase Data Connect) is a relational database service built on Cloud SQL for PostgreSQL, offering a GraphQL schema, auto-generated queries and mutations, and type-safe SDKs. This skill guides users through designing schemas with tables and relations, writing authorized queries and mutations, configuring real-time data updates, and generating type-safe SDKs. The skill covers project structure (dataconnect directory), development workflow (defining data model, operations, SDK generation), deployment and CLI commands, and a feature capability map (e.g., data modeling, vector search, full-text search, transactions, real-time subscriptions). It defaults to native GraphQL operations, using native SQL only when advanced database features (e.g., PostGIS, window functions) are required.

설치 수 · 669출처 보기

Installation

npx skills add https://github.com/firebase/agent-skills --skill firebase-data-connect

스킬 파일

SKILL.md

최근 동기화 · 2026. 8. 29.

examples.md
# Examples

Complete, working examples for common SQL Connect use cases.

______________________________________________________________________

## Movie Review App

A complete schema for a movie database with reviews, actors, and user
authentication.

### Schema

```graphql
# schema.gql

# Users
type User @table(key: "uid") {
  uid: String! @default(expr: "auth.uid")
  email: String! @unique
  displayName: String
  createdAt: Timestamp! @default(expr: "request.time")
}

# Movies
type Movie @table {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int
  genre: String @index
  rating: Float
  description: String
  posterUrl: String
  createdAt: Timestamp! @default(expr: "request.time")
}

# Movie metadata (one-to-one)
type MovieMetadata @table {
  movie: Movie! @unique
  director: String
  runtime: Int
  budget: Int64
}

# Actors
type Actor @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String!
  birthDate: Date
}

# Movie-Actor relationship (many-to-many)
type MovieActor @table(key: ["movie", "actor"]) {
  movie: Movie!
  actor: Actor!
  role: String!  # "lead" or "supporting"
  character: String
}

# Reviews (user-owned)
type Review @table @unique(fields: ["movie", "user"]) {
  id: UUID! @default(expr: "uuidV4()")
  movie: Movie!
  user: User!
  rating: Int!
  text: String
  createdAt: Timestamp! @default(expr: "request.time")
}
```

### Queries

```graphql
# queries.gql

# Public: List movies with filtering
query ListMovies($genre: String, $minRating: Float, $limit: Int) 
  @auth(level: PUBLIC) {
  movies(
    where: {
      genre: { eq: $genre },
      rating: { ge: $minRating }
    },
    orderBy: [{ rating: DESC }],
    limit: $limit
  ) {
    id title genre rating releaseYear posterUrl
  }
}

# Public: Get movie with full details
query GetMovie($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id title genre rating releaseYear description
    metadata: movieMetadata_on_movie { director runtime }
    actors: actors_via_MovieActor { name }
    reviews: reviews_on_movie(orderBy: [{ createdAt: DESC }], limit: 10) {
      rating text createdAt
      user { displayName }
    }
  }
}

# User: Get my reviews
query MyReviews @auth(level: USER) {
  reviews(where: { user: { uid: { eq_expr: "auth.uid" }}}) {
    id rating text createdAt
    movie { id title posterUrl }
  }
}
```

### Mutations

```graphql
# mutations.gql

# User: Create/update profile on first login
mutation UpsertUser($email: String!, $displayName: String) @auth(level: USER) {
  user_upsert(data: {
    uid_expr: "auth.uid",
    email: $email,
    displayName: $displayName
  })
}

# User: Add review (one per movie per user)
mutation AddReview($movieId: UUID!, $rating: Int!, $text: String) 
  @auth(level: USER) {
  review_upsert(data: {
    movie: { id: $movieId },
    user: { uid_expr: "auth.uid" },
    rating: $rating,
    text: $text
  })
}

# User: Delete my review
mutation DeleteReview($id: UUID!) @auth(level: USER) {
  review_delete(
    first: { where: {
      id: { eq: $id },
      user: { uid: { eq_expr: "auth.uid" }}
    }}
  )
}
```

### Realtime Queries

```graphql
# queries.gql (realtime additions)

# Auto-refresh: this single-entity lookup refreshes automatically
# when any mutation modifies this specific movie. No @refresh needed.
query GetMovie($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id title genre rating releaseYear description
    metadata: movieMetadata_on_movie { director runtime }
    reviews: reviews_on_movie(orderBy: [{ createdAt: DESC }], limit: 10) {
      rating text createdAt
      user { displayName }
    }
  }
}

# Event-driven: Simple refresh when any movie is added
query ListMoviesSimple @auth(level: PUBLIC) @refresh(onMutationExecuted: { operation: "AddMovie" }) {
  movies { id title }
}

# Counterpart mutation for ListMoviesSimple
mutation AddMovie($title: String!) @auth(level: USER) {
  movie_insert(data: { title: $title })
}

# Event-driven: Refresh only when a movie of the same genre is added
# Demonstrates the use of 'condition' and 'mutation.variables'
query ListMoviesByGenre($genre: String!) @auth(level: PUBLIC)
  @refresh(onMutationExecuted: {
    operation: "AddMovieWithGenre",
    condition: "mutation.variables.genre == request.variables.genre"
  }) {
  movies(where: { genre: { eq: $genre } }) { id title }
}

# Counterpart mutation for ListMoviesByGenre
mutation AddMovieWithGenre($title: String!, $genre: String!) @auth(level: USER) {
  movie_insert(data: { title: $title, genre: $genre })
}

# Event-driven: Refresh user profile when updated
# Demonstrates condition based on auth context
query MyProfile @auth(level: USER)
  @refresh(onMutationExecuted: {
    operation: "UpdateProfile",
    condition: "mutation.auth.uid == request.auth.uid"
  }) {
  user(uid_expr: "auth.uid") { id name }
}

# Counterpart mutation for MyProfile
mutation UpdateProfile($name: String!) @auth(level: USER) {
  user_update(id_expr: "auth.uid", data: { name: $name })
}

# Time-based: live leaderboard refreshing every 30 seconds
query MovieLeaderboard
  @auth(level: PUBLIC)
  @refresh(every: { seconds: 30 }) {
  movies(orderBy: [{ rating: DESC }], limit: 10) {
    id title rating
  }
}
```

```typescript
import { listMoviesRef, movieLeaderboardRef } from '@movie-app/dataconnect';
import { subscribe } from 'firebase/data-connect';

// Subscribe to movie list — refreshes when AddReview mutation runs
const unsubMovies = subscribe(listMoviesRef({ genre: 'Action' }), {
  onNext: (result) => updateMovieList(result.data.movies),
  onError: (error) => console.error(error)
});

// Subscribe to leaderboard — refreshes every 30 seconds
const unsubLeaderboard = subscribe(movieLeaderboardRef(), {
  onNext: (result) => updateLeaderboard(result.data.movies),
  onError: (error) => console.error(error)
});

// Cleanup
// unsubMovies();
// unsubLeaderboard();
```

______________________________________________________________________

## E-Commerce Store

Products, orders, and cart management with user authentication.

### Schema

```graphql
# schema.gql

type User @table(key: "uid") {
  uid: String! @default(expr: "auth.uid")
  email: String! @unique
  name: String
  shippingAddress: String
}

type Product @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String! @index
  description: String
  price: Float!
  stock: Int! @default(value: 0)
  category: String @index
  imageUrl: String
}

type CartItem @table(key: ["user", "product"]) {
  user: User!
  product: Product!
  quantity: Int!
}

enum OrderStatus {
  PENDING
  PAID
  SHIPPED
  DELIVERED
  CANCELLED
}

type Order @table {
  id: UUID! @default(expr: "uuidV4()")
  user: User!
  status: OrderStatus! @default(value: PENDING)
  total: Float!
  shippingAddress: String!
  createdAt: Timestamp! @default(expr: "request.time")
}

type OrderItem @table {
  id: UUID! @default(expr: "uuidV4()")
  order: Order!
  product: Product!
  quantity: Int!
  priceAtPurchase: Float!
}
```

### Operations

```graphql
# Public: Browse products
query ListProducts($category: String, $search: String) @auth(level: PUBLIC) {
  products(where: {
    category: { eq: $category },
    name: { contains: $search },
    stock: { gt: 0 }
  }) {
    id name price stock imageUrl
  }
}

# User: View cart
query MyCart @auth(level: USER) {
  cartItems(where: { user: { uid: { eq_expr: "auth.uid" }}}) {
    quantity
    product { id name price imageUrl stock }
  }
}

# User: Add to cart
mutation AddToCart($productId: UUID!, $quantity: Int!) @auth(level: USER) {
  cartItem_upsert(data: {
    user: { uid_expr: "auth.uid" },
    product: { id: $productId },
    quantity: $quantity
  })
}

# User: Checkout (transactional)
mutation Checkout($shippingAddress: String!) 
  @auth(level: USER) 
  @transaction {
  # Query cart items
  query @redact {
    cartItems(where: { user: { uid: { eq_expr: "auth.uid" }}}) 
      @check(expr: "this.size() > 0", message: "Cart is empty") {
      quantity
      product { id price }
    }
  }
  # Create order (in real app, calculate total from cart)
  order_insert(data: {
    user: { uid_expr: "auth.uid" },
    shippingAddress: $shippingAddress,
    total: 0  # Calculate in app logic
  })
}
```

______________________________________________________________________

## Blog with Permissions

Multi-author blog with role-based permissions.

### Schema

```graphql
# schema.gql

type User @table(key: "uid") {
  uid: String! @default(expr: "auth.uid")
  email: String! @unique
  name: String!
  bio: String
}

enum UserRole {
  VIEWER
  AUTHOR
  EDITOR
  ADMIN
}

type BlogPermission @table(key: ["user"]) {
  user: User!
  role: UserRole! @default(value: VIEWER)
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type Post @table {
  id: UUID! @default(expr: "uuidV4()")
  author: User!
  title: String! @searchable
  content: String! @searchable
  status: PostStatus! @default(value: DRAFT)
  publishedAt: Timestamp
  createdAt: Timestamp! @default(expr: "request.time")
  updatedAt: Timestamp! @default(expr: "request.time")
}

type Comment @table {
  id: UUID! @default(expr: "uuidV4()")
  post: Post!
  author: User!
  content: String!
  createdAt: Timestamp! @default(expr: "request.time")
}
```

### Operations with Role Checks

```graphql
# Public: Read published posts
query PublishedPosts @auth(level: PUBLIC) {
  posts(
    where: { status: { eq: PUBLISHED }},
    orderBy: [{ publishedAt: DESC }]
  ) {
    id title content publishedAt
    author { name }
  }
}

# Author+: Create post
mutation CreatePost($title: String!, $content: String!) 
  @auth(level: USER) 
  @transaction {
  # Check user is at least AUTHOR
  query @redact {
    blogPermission(key: { user: { uid_expr: "auth.uid" }})
      @check(expr: "this != null", message: "No permission record") {
      role @check(expr: "this in ['AUTHOR', 'EDITOR', 'ADMIN']", message: "Must be author+")
    }
  }
  post_insert(data: {
    author: { uid_expr: "auth.uid" },
    title: $title,
    content: $content
  })
}

# Editor+: Publish any post
mutation PublishPost($id: UUID!) 
  @auth(level: USER) 
  @transaction {
  query @redact {
    blogPermission(key: { user: { uid_expr: "auth.uid" }}) {
      role @check(expr: "this in ['EDITOR', 'ADMIN']", message: "Must be editor+")
    }
  }
  post_update(id: $id, data: {
    status: PUBLISHED,
    publishedAt_expr: "request.time"
  })
}

# Admin: Grant role
mutation GrantRole($userUid: String!, $role: UserRole!) 
  @auth(level: USER) 
  @transaction {
  query @redact {
    blogPermission(key: { user: { uid_expr: "auth.uid" }}) {
      role @check(expr: "this == 'ADMIN'", message: "Must be admin")
    }
  }
  blogPermission_upsert(data: {
    user: { uid: $userUid },
    role: $role
  })
}
```

______________________________________________________________________

## Native SQL Examples

For scenarios where standard GraphQL cannot express the required database logic,
use Native SQL.

### Basic SELECT with field aliasing

```graphql
query GetMoviesByGenre($genre: String!, $limit: Int!) @auth(level: PUBLIC) {
  movies: _select(
    sql: """
      SELECT id, title, release_year, rating
      FROM movie
      WHERE genre = $1
      ORDER BY release_year DESC
      LIMIT $2
    """,
    params: [$genre, $limit]
  )
}
```

### Basic UPDATE

```graphql
mutation UpdateMovieRating($movieId: UUID!, $newRating: Float!) @auth(level: USER) {
  _execute(
    sql: """
      UPDATE movie
      SET rating = $2
      WHERE id = $1
    """,
    params: [$movieId, $newRating]
  )
}
```

### Advanced aggregation with RANK

```graphql
query GetMoviesRankedByRating @auth(level: PUBLIC) {
  _select(
    sql: """
      SELECT
        id,
        title,
        rating,
        RANK() OVER (ORDER BY rating DESC) as rank
      FROM movie
      WHERE rating IS NOT NULL
      LIMIT 20
    """,
    params: []
  )
}
```

### UPDATE with RETURNING and Auth Context

```graphql
mutation UpdateMyReviewText($movieId: UUID!, $newText: String!) @auth(level: USER) {
  updatedReview: _executeReturningFirst(
    sql: """
      UPDATE review
      SET text = $2
      WHERE movie_id = $1 AND user_uid = $3
      RETURNING movie_id, user_uid, rating, text
    """,
    params: [$movieId, $newText, {_expr: "auth.uid"}]
  )
}
```

### Advanced CTE with upserts (atomic get-or-create)

*Note: Data-modifying CTEs are only supported by `_execute`, not
`_executeReturning`.*

```graphql
mutation CreateMovieCTE($movieId: UUID!, $userUid: String!, $reviewId: UUID!) @auth(level: USER) {
  _execute(
    sql: """
      WITH
      new_user AS (
        INSERT INTO "user" (uid, email, display_name)
        VALUES ($2, '[email protected]', 'Auto-Generated User')
        ON CONFLICT (uid) DO NOTHING
        RETURNING uid
      ),
      movie AS (
        INSERT INTO movie (id, title, poster_url, release_year, genre)
        VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      )
      INSERT INTO review (id, movie_id, user_uid, rating, text, created_at)
      VALUES (
        $3,
        $1,
        $2,
        5,
        'Good!',
        NOW()
      )
    """,
    params: [$movieId, $userUid, $reviewId]
  )
}
```

### Multi-statement Transactions

Because `mutation` operations are single requests, you can chain multiple
`_execute` commands within a `@transaction` to ensure they all succeed or fail
together.

```graphql
mutation SafeTransfer($from: UUID!, $to: UUID!, $amount: Float!) @auth(level: USER) @transaction {
  deduct: _execute(
    sql: "UPDATE account SET balance = balance - $2 WHERE id = $1", 
    params: [$from, $amount]
  )
  add: _execute(
    sql: "UPDATE account SET balance = balance + $2 WHERE id = $1", 
    params: [$to, $amount]
  )
}
```

### Use of extensions (e.g. PostGIS for geospatial data)

*Prerequisite:* You must enable the extension on your underlying Cloud SQL
instance by connecting to your database as the postgres user and running:

```sql
CREATE EXTENSION IF NOT EXISTS postgis;
```

```graphql
query GetNearbyActiveRestaurants($userLong: Float!, $userLat: Float!, $maxDistanceMeters: Float!) @auth(level: USER) {
  nearby: _select(
    sql: """
      SELECT 
        id, 
        name,
        tags,
        ST_Distance(
          ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography, 
          ST_MakePoint($1, $2)::geography
        ) as distance_meters
      FROM restaurant
      WHERE active = true
        AND metadata ? 'longitude' AND metadata ? 'latitude'
        AND ST_DWithin(
          ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography, 
          ST_MakePoint($1, $2)::geography, 
          $3
        )
      ORDER BY distance_meters ASC
      LIMIT 10
    """,
    params: [$userLong, $userLat, $maxDistanceMeters]
  )
}
```

*After running the query using a client SDK, the result will be in
`data.nearby`.*
reference/cloud_functions.md
# Cloud Functions Integration Reference

Use this reference to handle database events in SQL Connect by triggering Cloud
Functions in response to mutation executions.

______________________________________________________________________

## Core Trigger Configuration

To handle a mutation execution, define the `onMutationExecuted` event handler.

### 🚨 Critical Infinite Loop Constraint

Unlike document-based database triggers (like Firestore or Realtime Database),
**SQL Connect event triggers do not provide a "before" snapshot of the data.**
Because SQL Connect proxies requests directly to PostgreSQL, "before" states
cannot be resolved transactionally.

- **Warning**: If `onMutationExecuted` executes a SQL Connect mutation, it can
  trigger another `onMutationExecuted` trigger in a cascading loop. Make sure
  that `onMutationExecuted` has a filter on `operation` to reduce the chance of
  infinite loops.
- **Rule**: Ensure that no mutation executed inside the function can ever
  trigger the handler itself, even indirectly.

### Location & Region Matching Rule

**The Cloud Function region option must match your SQL Connect service
location.** You **must** explicitly configure the `region` parameter (e.g.,
`'us-central1'`) in the trigger options to match the `location` specified in
`dataconnect.yaml`.

```typescript
import { onMutationExecuted } from "firebase-functions/dataconnect";
import { logger } from "firebase-functions";

export const logMutation = onMutationExecuted(
  {
    region: "europe-west1" // Must match the SQL Connect service location
  },
  (event) => {
    logger.info("A mutation was executed!", {
      eventId: event.id,
      type: event.type
    });
  }
);
```

______________________________________________________________________

## Event Filtering

To prevent unnecessary function invocations and infinite execution loops,
**always specify narrow filters** using `service` and `operation` attributes.

- **`service` & `operation` (Recommended)**: Always specify these to restrict
  the trigger to a specific mutation in your project.
- **`connector` (Optional)**: Can be omitted if you want to trigger on the same
  operation name across multiple connectors. Specify it only if you need to
  restrict the trigger to a specific connector.

### Comprehensive Example

```typescript
import { onMutationExecuted } from "firebase-functions/dataconnect";
import { logger } from "firebase-functions";

// Triggers for "CreateUser" mutation in "myAppService" service.
// 'connector' is omitted (optional), meaning it matches "CreateUser" in any connector.
export const onUserCreate = onMutationExecuted(
  {
    service: "myAppService",
    operation: "CreateUser",
    // region: "us-central1" // Optional: defaults to us-central1, change if database is elsewhere
  },
  (event) => {
    logger.info("A new user was created!");
  }
);

// Advanced: Trigger using wildcards or capture variables
export const onMutationCaptures = onMutationExecuted(
  {
    service: "myAppService",
    operation: "{operation}", // Captures matching operation name dynamically
  },
  (event) => {
    const triggeredOp = event.params.operation;
    logger.info(`Captured operation execution: ${triggeredOp}`);
  }
);
```

______________________________________________________________________

## Accessing User Authentication Context

Extract security credentials about the caller who executed the mutation using
`event.authType` and `event.authId`.

### Auth Context Mappings

| Triggered Principal                  | `event.authType`    | `event.authId`                                   |
| :----------------------------------- | :------------------ | :----------------------------------------------- |
| **Authenticated end user**           | `"app_user"`        | Firebase Auth token UID                          |
| **Unauthenticated end user**         | `"unauthenticated"` | Empty                                            |
| **Admin SDK (Impersonating User)**   | `"app_user"`        | Firebase Auth token UID of the impersonated user |
| **Admin SDK (Impersonating Unauth)** | `"unauthenticated"` | Empty                                            |
| **Admin SDK (Full privileges)**      | `"admin"`           | Empty                                            |

### Auth Extraction Example

```typescript
export const processSensitiveMutation = onMutationExecuted(
  { operation: "UpdateFinancials" },
  (event) => {
    if (event.authType === "admin") {
      console.log("Elevated admin mutation execution.");
    } else {
      console.log(`Mutation initiated by user: ${event.authId}`);
    }
  }
);
```

______________________________________________________________________

## Parsing Event Data Payloads

The trigger payload provides inputs passed to the mutation (`payload.variables`)
and return values generated from the execution (`payload.data`).

### Event Payload Structure

```json
{
  "authType": "app_user",
  "authId": "user-123",
  "data": {
    "payload": {
      "variables": {
        "movieId": "m-1",
        "rating": 5
      },
      "data": {
        "review_insert": {
          "id": "r-99"
        }
      },
      "errors": []
    }
  }
}
```

- **`event.data.payload.variables`**: Inputs passed to the mutation.
- **`event.data.payload.data`**: Fields returned by the mutation execution.
- **`event.data.payload.errors`**: Array of execution errors. Empty if
  successful.

### Payload Extraction Example

```typescript
import { onMutationExecuted } from "firebase-functions/dataconnect";
import { logger } from "firebase-functions";

export const onNewReview = onMutationExecuted(
  {
    service: "myAppService",
    connector: "reviews",
    operation: "CreateReview",
  },
  (event) => {
    // Extract input variables passed to the mutation
    const inputVariables = event.data.payload.variables;

    // Extract returned fields from the database write
    const returnedFields = event.data.payload.data;

    logger.info(`Processed review ${returnedFields.review_insert.id} for movie ${inputVariables.movieId}`);
  }
);
```
reference/config.md
# Configuration Reference

## Contents

- [Project Structure](#project-structure)
- [dataconnect.yaml](#dataconnectyaml)
- [connector.yaml](#connectoryaml)
- [Firebase CLI Commands](#firebase-cli-commands)
- [Emulator](#emulator)
- [Deployment](#deployment)

______________________________________________________________________

## Project Structure

```
project-root/
├── firebase.json           # Firebase project config
└── dataconnect/
    ├── dataconnect.yaml    # Service configuration
    ├── schema/
    │   └── schema.gql      # Data model (types, relationships)
    └── connector/
        ├── connector.yaml  # Connector config + SDK generation
        ├── queries.gql     # Query operations
        └── mutations.gql   # Mutation operations (optional separate file)
```

______________________________________________________________________

## dataconnect.yaml

Main SQL Connect service configuration:

```yaml
specVersion: "v1"
serviceId: "my-service"
location: "us-central1"
schemaValidation: "STRICT" # or "COMPATIBLE"
schema:
  source: "./schema"
  datasource:
    postgresql:
      database: "fdcdb"
      cloudSql:
        instanceId: "my-instance"
connectorDirs: ["./connector"]
```

| Field               | Description                                                                              |
| ------------------- | ---------------------------------------------------------------------------------------- |
| `specVersion`       | Always `"v1"`                                                                            |
| `serviceId`         | Unique identifier for the service                                                        |
| `location`          | GCP region (us-central1, us-east4, europe-west1, etc.)                                   |
| `schemaValidation`  | Deployment mode: `"STRICT"` (must match exactly) or `"COMPATIBLE"` (backward compatible) |
| `schema.source`     | Path to schema directory                                                                 |
| `schema.datasource` | PostgreSQL connection config                                                             |
| `connectorDirs`     | List of connector directories                                                            |

### Cloud SQL Configuration

```yaml
schema:
  datasource:
    postgresql:
      database: "my-database"      # Database name
      cloudSql:
        instanceId: "my-instance"  # Cloud SQL instance ID
```

______________________________________________________________________

## connector.yaml

Connector configuration and SDK generation:

```yaml
connectorId: "default"
generate:
  javascriptSdk:
    outputDir: "../web/src/lib/dataconnect"
    package: "@myapp/dataconnect"
  kotlinSdk:
    outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect"
    package: "com.myapp.dataconnect"
  swiftSdk:
    outputDir: "../ios/MyApp/DataConnect"
```

### SDK Generation Options

| SDK             | Fields                                 |
| --------------- | -------------------------------------- |
| `javascriptSdk` | `outputDir`, `package`                 |
| `kotlinSdk`     | `outputDir`, `package`                 |
| `swiftSdk`      | `outputDir`                            |
| `nodeAdminSdk`  | `outputDir`, `package` (for Admin SDK) |

______________________________________________________________________

## Firebase CLI Commands

### Initialize SQL Connect

```bash
# Interactive setup
npx -y firebase-tools@latest init dataconnect

# Set project
npx -y firebase-tools@latest use <project-id>
```

### Local Development

```bash
# Start emulator
npx -y firebase-tools@latest emulators:start --only dataconnect

# Start with database seed data
npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data

# Generate SDKs
npx -y firebase-tools@latest dataconnect:sdk:generate

# Watch for schema changes (auto-regenerate)
npx -y firebase-tools@latest dataconnect:sdk:generate --watch
```

### Schema Management

```bash
# Compare local schema to production
npx -y firebase-tools@latest dataconnect:sql:diff


# Apply migration
npx -y firebase-tools@latest dataconnect:sql:migrate
```

### Deployment

```bash
# Deploy SQL Connect service
npx -y firebase-tools@latest deploy --only dataconnect

# Deploy specific connector
npx -y firebase-tools@latest deploy --only dataconnect:connector-id

# Deploy with schema migration
npx -y firebase-tools@latest deploy --only dataconnect --force
```

______________________________________________________________________

## Emulator

### Start Emulator

```bash
npx -y firebase-tools@latest emulators:start --only dataconnect
```

Default ports:

- SQL Connect: `9399`
- PostgreSQL: `9939` (local PostgreSQL instance)

### Emulator Configuration (firebase.json)

```json
{
  "emulators": {
    "dataconnect": {
      "port": 9399
    }
  }
}
```

### Connect from SDK

```typescript
// Web
import { connectDataConnectEmulator } from 'firebase/data-connect';
connectDataConnectEmulator(dc, 'localhost', 9399);

// Android
connector.dataConnect.useEmulator("10.0.2.2", 9399)

// iOS
connector.useEmulator(host: "localhost", port: 9399)


```

### Seed Data

Create seed data files and import:

```bash
# Export current emulator data
npx -y firebase-tools@latest emulators:export ./seed-data

# Start with seed data
npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data
```

______________________________________________________________________

## Deployment

### Deploy Workflow

1. **Test locally** with emulator
1. **Generate SQL diff**: `npx -y firebase-tools@latest dataconnect:sql:diff`
1. **Review migration**: Check breaking changes
1. **Deploy**: `npx -y firebase-tools@latest deploy --only dataconnect`

### Schema Migrations

SQL Connect auto-generates PostgreSQL migrations:

```bash
# Preview migration
npx -y firebase-tools@latest dataconnect:sql:diff

# Apply migration (interactive)
npx -y firebase-tools@latest dataconnect:sql:migrate

# Force migration (non-interactive)
npx -y firebase-tools@latest dataconnect:sql:migrate --force
```

### Breaking Changes

Some schema changes require special handling:

- Removing required fields
- Changing field types
- Removing tables

Use `--force` flag to acknowledge breaking changes during deploy.

### CI/CD Integration

```yaml
# GitHub Actions example
- name: Deploy SQL Connect
  run: |
    npx -y firebase-tools@latest deploy --only dataconnect --token ${{ secrets.FIREBASE_TOKEN }} --force
```

______________________________________________________________________

## VS Code Extension

Install "Firebase SQL Connect" extension for:

- Schema intellisense and validation
- GraphQL operation testing
- Emulator integration
- SDK generation on save

### Extension Settings

```json
{
  "firebase.dataConnect.autoGenerateSdk": true,
  "firebase.dataConnect.emulator.port": 9399
}
```
reference/data_seeding.md
# Data Seeding & Bulk Operations Reference

Use this reference to populate local development databases for prototyping,
execute CI/CD tests, and perform bulk data migrations in production
environments.

______________________________________________________________________

## 1. Local Prototyping: Data Seeding

Local database seeding allows developer agents to test queries, mutations,
complex joins, and role-based access control (RBAC) under realistic conditions.

### The `seed_data.gql` Workflow

**Always write prototyping seed mutations to `dataconnect/seed_data.gql`**
(located at the project root, not inside `connector/`). This file is excluded
from production deployments and client SDK generation.

#### ⚠️ Seeding Directives Rule

**Do not declare `@auth` directives inside `seed_data.gql` mutations.** Since
this file runs locally to establish a test state and is not an exposed API
connector endpoint, authorization directives are completely unnecessary and
should be omitted.

### Seeding Independent Tables (FK Order)

When executing standard bulk insertions (`_insertMany`) across multiple tables,
**always insert parent tables before referencing them in child or join tables.**

```graphql
# dataconnect/seed_data.gql
mutation SeedIndependentTables @transaction {
  # Step 1: Seed parent tables
  movie_insertMany(data: [
    { id: "m-1", title: "Inception", genre: "sci-fi" },
    { id: "m-2", title: "The Matrix", genre: "action" }
  ])

  actor_insertMany(data: [
    { id: "a-1", name: "Leonardo DiCaprio" },
    { id: "a-2", name: "Keanu Reeves" }
  ])

  # Step 2: Seed join table (depends on pre-existing parent IDs)
  movieActor_insertMany(data: [
    { movie: { id: "m-1" }, actor: { id: "a-1" }, role: "main" },
    { movie: { id: "m-2" }, actor: { id: "a-2" }, role: "main" }
  ])
}
```

### Seeding Related Tables (Nested Relational Inserts)

**To seed parent-child relationships atomically, perform a nested relational
insert using literal payloads.** This avoids the need to manage foreign keys
manually.

- **Omit Parent Foreign Keys**: **Do not specify the parent foreign key** (e.g.
  `movieId`) inside the nested child objects. The database engine automatically
  maps and resolves them.

```graphql
# dataconnect/seed_data.gql
mutation SeedMoviesAndReviews @transaction {
  movie_insert(data: {
    id: "m-1",
    title: "Inception",
    genre: "sci-fi",
    # Nested reviews are inserted atomically without manual movieId mapping
    reviews_on_movie: [
      {
        id: "r-1",
        rating: 5,
        reviewText: "Mind-bending masterpiece!",
        user: { id: "user-123" } # Links to pre-existing user
      },
      {
        id: "r-2",
        rating: 4,
        reviewText: "Visually stunning but complex.",
        user: { id: "user-456" }
      }
    ]
  })
}
```

### Resetting Seed Data

For continuous testing or CI/CD flows, return the database to a zero state using
one of the following strategies:

- **Strategy A: Upsert Many (Idempotent)**: Re-run seeds using `_upsertMany`
  mutations. This overrides existing records or inserts missing ones in a single
  step.
- **Strategy B: Delete and Re-Insert**: Call `_deleteMany(all: true)` on your
  tables in **reverse foreign key order** (child/join tables first, then parent
  tables) followed by your seed `_insertMany` operations.

```graphql
# dataconnect/seed_data.gql
mutation ResetDatabaseToOriginalState @transaction {
  # Delete child tables first to prevent FK constraint violations
  movieActor_deleteMany(all: true)
  actor_deleteMany(all: true)
  movie_deleteMany(all: true)
  # (Optional) Follow up with new _insertMany steps
}
```

______________________________________________________________________

## 2. Production: Admin SDK Bulk Operations

**Use the Firebase Admin SDK for Node.js for bulk data loading and production
migrations.** Avoid running large mutations directly via raw GraphQL endpoints
in production.

The Admin SDK provides direct, type-safe methods: `dc.insert`, `dc.insertMany`,
`dc.upsert`, and `dc.upsertMany`.

### SDK Bulk APIs Features:

- **No Manual GraphQL Strings**: Do not write raw `mutation {...}` strings when
  executing privileged batch operations. Pass Javascript objects directly.
- **Relational Support**: The bulk helper methods natively support nested 1:Many
  relationships inside the input arrays.

### SDK Bulk Operations Example

```typescript
import { initializeApp } from 'firebase-admin/app';
import { getDataConnect } from 'firebase-admin/data-connect';

const app = initializeApp();
const dc = getDataConnect({ location: "us-west2", serviceId: "my-service" });

const bulkMoviesData = [
  {
    id: "m-1",
    title: "Inception",
    genre: "sci-fi",
    // Atomic nested relational inserts are fully supported
    reviews_on_movie: [
      {
        rating: 5,
        reviewText: "Incredible concept.",
        user: { id: "user-123" }
      }
    ]
  },
  {
    id: "m-2",
    title: "The Matrix",
    genre: "action",
    reviews_on_movie: [
      {
        rating: 5,
        reviewText: "A classic.",
        user: { id: "user-456" }
      }
    ]
  }
];

// Atomically load thousands of records (parent and child tables combined)
const response = await dc.insertMany("movie", bulkMoviesData);
```

______________________________________________________________________

## 3. Production: Bulk Operations via raw SQL

When working with a stable schema in production, you can use standard SQL tools
(like `psql` or Cloud SQL import pipelines) to execute bulk data updates
directly on the PostgreSQL instance.

### 🚨 Critical SQL Operations Constraint

**Never modify your database schema directly using SQL tools.** Direct schema
alterations (`ALTER TABLE`, `CREATE INDEX`, etc.) outside of your `schema.gql`
file will bypass SQL Connect's schema compiler, breaking connector mappings, and
causing active client SDK integrations to fail.
reference/native_sql.md
# Native SQL Operations

Always default to Native GraphQL. Use Native SQL **only** when you need
database-specific features not available in GraphQL (e.g., PostGIS, Window
Functions, Complex Aggregations, or specific DML CTEs).

## Core Agent Constraints

When generating Native SQL operations, you are bypassing GraphQL and talking
directly to PostgreSQL. You **MUST** adhere to these strict constraints:

1. **Operation Syntax Isolation:** Never mix Native SQL positional parameters
   (`$1`) with standard GraphQL named variables (`$id`). The `sql:` argument
   MUST be a hardcoded string literal block (`"""SELECT..."""`), not a GraphQL
   variable.
1. **Table & Column Mapping (Case Sensitivity):**
   - **Default `snake_case` Conversion:** By default, SQL Connect converts
     `PascalCase` types and `camelCase` fields to `snake_case` in the database.
     - *Schema:* `type UserProfile { releaseYear: Int }` -> *Native SQL:*
       `SELECT release_year FROM user_profile`
   - **Explicit Overrides (Requires Double Quotes):** If the schema uses
     `@table(name: "ExactName")` or `@col(name: "ExactCol")`, you **MUST wrap
     the identifier in double quotes** if it contains capital letters (e.g.,
     `SELECT * FROM "ExactName"`). Without quotes, Postgres folds it to
     lowercase and fails validation.

## Syntax rules & limitations

Native SQL enforces strict parsing rules to ensure security and prevent SQL
injection:

- **String Literals Only:** The `sql` argument must be a hardcoded string
  literal block (`"""SELECT..."""`) directly in the `.gql` file. It **cannot**
  be a GraphQL variable.
- **Validation:** Do **NOT** use DDL in any operations (modify the `schema.gql`
  file instead for table/column changes). Furthermore, `query` operations cannot
  contain DML and must start with `SELECT`, `TABLE`, or `WITH`.
- **Parameters:** Use strict positional parameters (`$1`, `$2`) that match the
  `params` array order. Named parameters (`$id`, `:name`) are **forbidden**.
- **Comments:** Use block comments (`/* ... */`). Line comments (`--`) are
  **forbidden** because they can truncate subsequent clauses during query
  compilation. If you comment out a line containing a parameter (e.g.,
  `/* WHERE id = $1 */`), you must also remove that parameter from the `params`
  list, or it will fail with `unused parameter: $1`.
- **Strings:** Extended string literals (`E'...'`) and dollar-quoted strings
  (`$$...$$`) are supported.
- **Context Maps (`_expr`):** Variables **cannot** be used inside `_expr`
  fields; to ensure security, `_expr` must be a static string (e.g.,
  `{_expr: "auth.uid"}`, not `{_expr: $uidVar}`).

## Native SQL Root Fields

Operations are executed using the permissions granted to the SQL Connect service
account. You can alias the root field (e.g., `movies: _select`) to make the
client response cleaner (`data.movies` instead of `data._select`).

> **Note on `Any` Return Types:** Because Native SQL completely bypasses
> GraphQL's strong typing, queries like `_select` and `_executeReturning` return
> the generic `Any` scalar type. The generated client SDKs (TypeScript, Swift,
> Kotlin, Dart) will type this as `any` (or equivalent). **AGENT INSTRUCTION**:
> When you generate client-side code that consumes these operations, you MUST
> manually cast or validate the shape of the data, as the typical type safety of
> SQL Connect will not be present.

Use these root fields in `query` or `mutation` operations:

### Query Fields (Read-Only)

- `_select`: Executes a SQL query returning zero or more rows. Returns `[Any]`.
  ```graphql
  query GetMovies($genre: String!) @auth(level: PUBLIC) {
    movies: _select(
      sql: "SELECT id, title FROM movie WHERE genre = $1",
      params: [$genre]
    )
  }
  ```
- `_selectFirst`: Executes a SQL query expected to return zero or one row.
  Returns `Any` or `null`.
  ```graphql
  query GetTotalReviews @auth(level: PUBLIC) {
    stats: _selectFirst(
      sql: "SELECT COUNT(*) as total_reviews FROM review"
    ) # params can be omitted if empty
  }
  ```

### Mutation Fields (DML)

- `_execute`: Executes DML (`INSERT`, `UPDATE`, `DELETE`). Returns `Int` (number
  of rows affected).
  - *Note 1:* `RETURNING` clauses are ignored in the result.
  - *Note 2:* Only `_execute` supports Data-Modifying Common Table Expressions
    (e.g., `WITH new_row AS (INSERT...)`).
  ```graphql
  mutation UpdateRating($id: UUID!, $rating: Float!) @auth(level: USER) {
    _execute(
      sql: "UPDATE movie SET rating = $2 WHERE id = $1",
      params: [$id, $rating]
    )
  }
  ```
- `_executeReturning`: Executes DML with a `RETURNING` clause. Returns `[Any]`.
  Data-Modifying CTEs are **not** supported.
  ```graphql
  mutation DeleteUserReviews($uid: String!) @auth(level: USER) {
    deletedReviews: _executeReturning(
      sql: "DELETE FROM review WHERE user_id = $1 RETURNING id, rating",
      params: [{_expr: "auth.uid"}]
    )
  }
  ```
- `_executeReturningFirst`: Executes DML with `RETURNING`, expecting zero or one
  row. Returns `Any` or `null`. Data-Modifying CTEs are **not** supported.
  ```graphql
  mutation UpdateMyReview($movieId: UUID!, $text: String!) @auth(level: USER) {
    updatedReview: _executeReturningFirst(
      sql: """
        UPDATE review SET text = $2 
        WHERE movie_id = $1 AND user_id = $3 
        RETURNING id, text
      """,
      params: [$movieId, $text, {_expr: "auth.uid"}]
    )
  }
  ```

### PostgreSQL Extensions

Native SQL allows you to directly query and utilize PostgreSQL extensions, such
as `PostGIS`, without needing to map complex geometry types into your GraphQL
schema or alter your underlying tables (e.g., using JSON operators to extract
values and pass them into `ST_MakePoint`).

*Note: You must enable the extension on your underlying Cloud SQL instance by
connecting as the `postgres` user and running
`CREATE EXTENSION IF NOT EXISTS ...;`*

*(See `examples.md` for a full `GetNearbyActiveRestaurants` implementation).*

## ⚠️ Security: Stored Procedures & Dynamic SQL

SQL Connect parameterizes inputs at the GraphQL boundary automatically. However,
if your Native SQL calls **custom PL/pgSQL stored procedures**, you must
manually prevent 2nd-order SQL injection:

- **NEVER** concatenate user input into an `EXECUTE` string
  (`EXECUTE 'UPDATE ' || table || ' SET x=' || val;`).
- **DO** use the `USING` clause to bind data values safely.
- **DO** use `format('%I')` for safe database identifier injection.
- **DO** validate dynamic table/column names against a strict hardcoded
  allowlist.

**Secure PL/pgSQL Pattern:**

```sql
CREATE OR REPLACE PROCEDURE secure_update(target_table TEXT, new_value TEXT, row_id INT)
LANGUAGE plpgsql AS $$
BEGIN
    -- 1. Strict Allowlist for Identifiers
    IF target_table NOT IN ('orders', 'users', 'inventory') THEN
        RAISE EXCEPTION 'Invalid table name';
    END IF;

    -- 2. format(%I) for Identifiers, USING for Data
    EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table)
    USING new_value, row_id;
END;
$$;
```
reference/operations.md
# Operations Reference

## Contents

- [Generated Fields](#generated-fields)
- [Queries](#queries)
- [Mutations](#mutations)
- [Key Scalars](#key-scalars)
- [Multi-Step Operations](#multi-step-operations)

______________________________________________________________________

## Generated Fields

SQL Connect auto-generates fields for each `@table` type:

| Generated Field                                                                         | Purpose             | Example                                          |
| --------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------ |
| `movie(id: UUID, key: Key, first: Row)`                                                 | Get single record   | `movie(id: $id)` or `movie(first: {where: ...})` |
| `movies(where: ..., orderBy: ..., limit: ..., offset: ..., distinct: ..., having: ...)` | List/filter records | `movies(where: {...})`                           |
| `movie_insert(data: ...)`                                                               | Create record       | Returns key                                      |
| `movie_insertMany(data: [...])`                                                         | Bulk create         | Returns keys                                     |
| `movie_update(id: ..., data: ...)`                                                      | Update by ID        | Returns key or null                              |
| `movie_updateMany(where: ..., data: ...)`                                               | Bulk update         | Returns count                                    |
| `movie_upsert(data: ...)`                                                               | Insert or update    | Returns key                                      |
| `movie_delete(id: ...)`                                                                 | Delete by ID        | Returns key or null                              |
| `movie_deleteMany(where: ...)`                                                          | Bulk delete         | Returns count                                    |

### Relation Fields

For a `Post` with `author: User!`:

- `post.author` - Navigate to related User
- `user.posts_on_author` - Reverse: all Posts by User

For many-to-many via `MovieActor`:

- `movie.actors_via_MovieActor` - Get all actors
- `actor.movies_via_MovieActor` - Get all movies

______________________________________________________________________

## Referencing Generated GraphQL Schema

**Do not guess** available queries or mutations. Review the generated schema
files instead of trying to deduce them from the data model.

1. **Location**: `.dataconnect/schema/main/` (relative to project root).
1. **Action**: Scan this directory for generated files (`query.gql`,
   `mutation.gql`, `relation.gql`, `input.gql`) to understand the exact shape of
   the API and auto-generated types.
1. **Validation**: Always run `firebase dataconnect:compile` to verify
   operations against the full schema.

______________________________________________________________________

## Queries

### Basic Query

```graphql
query GetMovie($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id title genre releaseYear
  }
}
```

### List with Filtering

```graphql
query ListMovies($genre: String, $minRating: Int) @auth(level: PUBLIC) {
  movies(
    where: {
      genre: { eq: $genre },
      rating: { ge: $minRating }
    },
    orderBy: [{ releaseYear: DESC }, { title: ASC }],
    limit: 20,
    offset: 0
  ) {
    id title genre rating
  }
}
```

### Filter Operators

| Operator     | Description             | Example                                     |
| ------------ | ----------------------- | ------------------------------------------- |
| `eq`         | Equals                  | `{ title: { eq: "Matrix" }}`                |
| `ne`         | Not equals              | `{ status: { ne: "deleted" }}`              |
| `gt`, `ge`   | Greater than (or equal) | `{ rating: { ge: 4 }}`                      |
| `lt`, `le`   | Less than (or equal)    | `{ releaseYear: { lt: 2000 }}`              |
| `in`         | In list                 | `{ genre: { in: ["Action", "Drama"] }}`     |
| `nin`        | Not in list             | `{ status: { nin: ["deleted", "hidden"] }}` |
| `isNull`     | Is null check           | `{ description: { isNull: true }}`          |
| `contains`   | String contains         | `{ title: { contains: "war" }}`             |
| `startsWith` | String starts with      | `{ title: { startsWith: "The" }}`           |
| `endsWith`   | String ends with        | `{ email: { endsWith: "@gmail.com" }}`      |
| `includes`   | Array includes          | `{ tags: { includes: "sci-fi" }}`           |

### Expression Operators (Compare with Server Values)

Use `_expr` suffix to compare with server-side values:

```graphql
query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id title
  }
}

query RecentPosts @auth(level: PUBLIC) {
  posts(where: { publishedAt: { lt_expr: "request.time" }}) {
    id title
  }
}
```

### Logical Operators

```graphql
query ComplexFilter($genre: String, $minRating: Int) @auth(level: PUBLIC) {
  movies(where: {
    _or: [
      { genre: { eq: $genre }},
      { rating: { ge: $minRating }}
    ],
    _and: [
      { releaseYear: { ge: 2000 }},
      { status: { ne: "hidden" }}
    ],
    _not: { genre: { eq: "Horror" }}
  }) { id title }
}
```

### Relational Queries

```graphql
# Navigate relationships
query MovieWithDetails($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    title
    # One-to-one
    metadata: movieMetadata_on_movie { director }
    # One-to-many
    reviews: reviews_on_movie { rating user { name }}
    # Many-to-many
    actors: actors_via_MovieActor { name }
  }
}

# Filter by related data
query MoviesByDirector($director: String!) @auth(level: PUBLIC) {
  movies(where: {
    movieMetadata_on_movie: { director: { eq: $director }}
  }) { id title }
}

# Filter by null relationship (e.g., top-level categories with no parent)
# Use the generated foreign key field (e.g., parentId)
query TopLevelCategories @auth(level: PUBLIC) {
  categories(where: { parentId: { eq: null } }) {
    id
    name
  }
}
```

### Aliases

```graphql
query CompareRatings($genre: String!) @auth(level: PUBLIC) {
  highRated: movies(where: { genre: { eq: $genre }, rating: { ge: 8 }}) {
    title rating
  }
  lowRated: movies(where: { genre: { eq: $genre }, rating: { lt: 5 }}) {
    title rating
  }
}
```

______________________________________________________________________

## Mutations

### Create

```graphql
mutation CreateMovie($title: String!, $genre: String) @auth(level: USER) {
  movie_insert(data: {
    title: $title,
    genre: $genre
  })
}
```

### Create with Server Values

```graphql
mutation CreatePost($title: String!, $content: String!) @auth(level: USER) {
  post_insert(data: {
    authorUid_expr: "auth.uid",         # Current user
    id_expr: "uuidV4()",                 # Auto-generate UUID
    createdAt_expr: "request.time",      # Server timestamp
    title: $title,
    content: $content
  })
}
```

### Update

```graphql
mutation UpdateMovie($id: UUID!, $title: String, $genre: String) @auth(level: USER) {
  movie_update(
    id: $id,
    data: {
      title: $title,
      genre: $genre,
      updatedAt_expr: "request.time"
    }
  )
}
```

### Update Operators

```graphql
mutation IncrementViews($id: UUID!) @auth(level: PUBLIC) {
  movie_update(id: $id, data: {
    viewCount_update: { inc: 1 }
  })
}

mutation AddTag($id: UUID!, $tag: String!) @auth(level: USER) {
  movie_update(id: $id, data: {
    tags_update: { add: [$tag] }  # add, remove, append, prepend
  })
}
```

| Operator  | Types                       | Description               |
| --------- | --------------------------- | ------------------------- |
| `inc`     | Int, Float, Date, Timestamp | Increment value           |
| `dec`     | Int, Float, Date, Timestamp | Decrement value           |
| `add`     | Lists                       | Add items if not present  |
| `remove`  | Lists                       | Remove all matching items |
| `append`  | Lists                       | Append to end             |
| `prepend` | Lists                       | Prepend to start          |

### Upsert

```graphql
mutation UpsertUser($email: String!, $name: String!) @auth(level: USER) {
  user_upsert(data: {
    uid_expr: "auth.uid",
    email: $email,
    name: $name
  })
}
```

### Delete

```graphql
mutation DeleteMovie($id: UUID!) @auth(level: USER) {
  movie_delete(id: $id)
}

mutation DeleteOldDrafts @auth(level: USER) {
  post_deleteMany(where: {
    status: { eq: "draft" },
    createdAt: { lt_time: { now: true, sub: { days: 30 }}}
  })
}
```

### Filtered Updates/Deletes (User-Owned)

```graphql
mutation UpdateMyPost($id: UUID!, $content: String!) @auth(level: USER) {
  post_update(
    first: { where: {
      id: { eq: $id },
      authorUid: { eq_expr: "auth.uid" }  # Only own posts
    }},
    data: { content: $content }
  )
}
```

______________________________________________________________________

## Key Scalars

Key scalars (`Movie_Key`, `User_Key`) are auto-generated types representing
primary keys:

```graphql
# Using key scalar
query GetMovie($key: Movie_Key!) @auth(level: PUBLIC) {
  movie(key: $key) { title }
}

# Variable format
# { "key": { "id": "uuid-here" } }

# Composite key
# { "key": { "movieId": "...", "userId": "..." } }
```

Key scalars are returned by mutations:

```graphql
mutation CreateAndFetch($title: String!) @auth(level: USER) {
  key: movie_insert(data: { title: $title })
  # Returns: { "key": { "id": "generated-uuid" } }
}
```

______________________________________________________________________

## Multi-Step Operations

### @transaction

Ensures atomicity - all steps succeed or all rollback:

```graphql
mutation CreateUserWithProfile($name: String!, $bio: String!) 
  @auth(level: USER) 
  @transaction {
  # Step 1: Create user
  user_insert(data: {
    uid_expr: "auth.uid",
    name: $name
  })
  # Step 2: Create profile (uses response from step 1)
  userProfile_insert(data: {
    userId_expr: "response.user_insert.uid",
    bio: $bio
  })
}
```

### Using response Binding

Access results from previous steps:

```graphql
mutation CreateTodoWithItem($listName: String!, $itemText: String!) 
  @auth(level: USER) 
  @transaction {
  todoList_insert(data: {
    id_expr: "uuidV4()",
    name: $listName
  })
  todoItem_insert(data: {
    listId_expr: "response.todoList_insert.id",  # From previous step
    text: $itemText
  })
}
```

### Embedded Queries

Run queries within mutations for validation:

```graphql
mutation AddToPublicList($listId: UUID!, $item: String!)
  @auth(level: USER)
  @transaction {
  # Step 1: Verify list exists and is public
  query @redact {
    todoList(id: $listId) @check(expr: "this != null", message: "List not found") {
      isPublic @check(expr: "this == true", message: "List is not public")
    }
  }
  # Step 2: Add item
  todoItem_insert(data: { listId: $listId, text: $item })
}
```
reference/realtime.md
# Realtime Reference

## Contents

- [When to Use What](#when-to-use-what)
- [The @refresh Directive](#the-refresh-directive)
- [CEL Bindings in Conditions](#cel-bindings-in-conditions)
- [Implicit Entity Refresh signals](#implicit-entity-refresh-signals)

______________________________________________________________________

## When to Use What

SQL Connect provides three mechanisms for live data updates. Pick the right one
based on what you're querying:

| Scenario                                                    | Mechanism                | Directive Needed?                   |
| ----------------------------------------------------------- | ------------------------ | ----------------------------------- |
| Single-entity lookup by ID (e.g., `movie(id: $id)`)         | **Automatic refresh**    | No — SQL Connect handles it         |
| List query that should update when a specific mutation runs | **Event-driven refresh** | `@refresh(onMutationExecuted: ...)` |
| Any query that should poll at a fixed interval              | **Time-based polling**   | `@refresh(every: ...)`              |

List queries require explicit `@refresh` to tell SQL Connect which mutations
affect the result set.

Clients consume all three using `subscribe()` instead of `execute()`. See
[sdks.md](sdks.md) for per-platform subscribe patterns.

______________________________________________________________________

## The @refresh Directive

`@refresh` is a **repeatable** directive applied to **queries**. It defines when
connected subscribers should receive updated data.

### Time-Based Polling (`every`)

Keep the query fresh with a recommended refresh interval. Note that `every` and
`mutation` signals can be used together; whichever signal arrives first will
trigger the refresh.

```graphql
query MovieLeaderboard
  @auth(level: PUBLIC)
  @refresh(every: { seconds: 30 }) {
  movies(orderBy: [{ rating: DESC }], limit: 10) {
    id title rating
  }
}
```

**Constraints:**

- The `every` argument takes a duration object: `{ seconds: Int }`
- **Minimum**: `{ seconds: 10 }` — protects against excessive server load
- **Maximum**: `{ hours: 1 }` (3600 seconds)
- Values outside this range fail validation at deploy time

Use time-based polling when freshness matters but you don't have a specific
mutation to listen for (e.g., dashboards aggregating external data, stock
tickers, activity feeds).

### Explicit Mutation Signals (`onMutationExecuted`)

Trigger a query refresh when a specific mutation executes. This is the most
common pattern for keeping lists in sync.

```graphql
# Example with condition (refreshes only when the condition is met)
query ChatRoom($roomId: UUID!) @auth(level: PUBLIC)
  @refresh(onMutationExecuted: {
    operation: "SendMessage",
    condition: "mutation.variables.roomId == request.variables.roomId"
  }) {
  messages(where: {roomId: {eq: $roomId}}, orderBy: [{createTime: DESC}], limit: 50) {
    author content createTime
  }
}

# Example without condition (refreshes on any execution of the named mutation)
query ListAllMessages
  @auth(level: PUBLIC)
  @refresh(onMutationExecuted: {
    operation: "SendMessage"
  }) {
  messages { id content }
}
```

**Arguments:**

- **`operation`** (required): The name of the mutation operation to listen for.
  Must match the mutation's operation name exactly.
- **`condition`** (optional): A CEL expression that must evaluate to `true` for
  the refresh to fire. Without a condition, every execution of the named
  mutation triggers a refresh.

It's highly recommended to define fine granular conditions. Inaccurate refresh
policies could consume Postgres resources and make your app slower.

Use conditions to scope refreshes precisely — a review list should only refresh
when the mutation targets the same movie, not every review across the entire
app.

### Combining Multiple @refresh Directives

Since `@refresh` is repeatable, you can combine strategies on a single query:

```graphql
query ActiveOrders($userId: UUID!)
  @auth(level: USER)
  @refresh(onMutationExecuted: {
    operation: "UpdateOrderStatus",
    condition: "request.variables.userId == mutation.variables.userId"
  })
  @refresh(every: { seconds: 60 }) {
  orders(where: { user: { id: { eq: $userId }}, status: { ne: DELIVERED }}) {
    id status total updatedAt
  }
}
```

This query refreshes whenever an order status changes for this user, *and* polls
every 60 seconds as a fallback to catch any updates that might not have a direct
mutation trigger.

______________________________________________________________________

## CEL Bindings in Conditions

The `condition` expression in `onMutationExecuted` has access to two contexts:

### `request` — The Query Subscription

The state of the query being subscribed to.

| Binding              | Description                                                  |
| -------------------- | ------------------------------------------------------------ |
| `request.variables`  | Variables passed to the query (e.g., `request.variables.id`) |
| `request.auth.uid`   | UID of the user who subscribed                               |
| `request.auth.token` | Full auth token claims of the subscriber                     |

### `mutation` — The Triggering Event

The mutation that just executed.

| Binding               | Description                                                           |
| --------------------- | --------------------------------------------------------------------- |
| `mutation.variables`  | Variables passed to the mutation (e.g., `mutation.variables.movieId`) |
| `mutation.auth.uid`   | UID of the user who executed the mutation                             |
| `mutation.auth.token` | Full auth token claims of the mutation executor                       |

### Common Patterns

```text
# Refresh only when the mutation targets the same entity
"request.variables.id == mutation.variables.id"

# Refresh only when the same user who subscribed makes a change
"request.auth.uid == mutation.auth.uid"

# Refresh when a specific field value matches a condition
"request.auth.uid == mutation.auth.uid && mutation.variables.status == 'PUBLISHED'"

# Refresh when a specific flag is set in the mutation
"mutation.variables.isPublic == true"
```

______________________________________________________________________

## Implicit Entity Refresh signals

For single-entity lookups by unique identifier, SQL Connect handles refreshes
automatically — no `@refresh` directive needed.

**What qualifies:**

- Queries fetching one entity by its primary key: `movie(id: $id)`,
  `user(key: { uid: $uid })`
- If a single-entity mutation modifies that specific entity, all active
  subscribers automatically receive the update. Supported operations include:
  - `_insert(data)` or `_insertMany(data)`
  - `_upsert(data)` or `_upsertMany(data)`
  - `_update(id)` or `_update(key)`
  - `_delete(id)` or `_delete(key)`
- **Note**: Bulk operations like `_updateMany` and `_deleteMany` do **not**
  trigger automatic entity refreshes.

**What does NOT qualify:**

- List queries: `movies(where: {...})`, `users { id name }` — these require
  explicit `@refresh`
- Nested query with JOINs
- Aggregation
- Native SQL
- Customized Resolver (if supported)

```graphql
# When subscribed to, this query auto-refreshes when movie data changes — no @refresh needed
query GetMovie($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id title rating description
    reviews_on_movie { rating text user { displayName } }
  }
}
```

To consume automatic refreshes on the client, use `subscribe()` instead of
`execute()` — the same client pattern works regardless of whether the refresh is
automatic or directive-driven.
reference/schema.md
# Schema Reference

## Contents

- [Defining Types](#defining-types)
- [Core Directives](#core-directives)
- [Relationships](#relationships)
- [Data Types](#data-types)
- [Enumerations](#enumerations)

______________________________________________________________________

## Defining Types

Types with `@table` map to PostgreSQL tables. SQL Connect auto-generates an
implicit `id: UUID!` primary key.

```graphql
type Movie @table {
  # id: UUID! is auto-added
  title: String!
  releaseYear: Int
  genre: String
}
```

### Customizing Tables

```graphql
type Movie @table(name: "movies", key: "id", singular: "movie", plural: "movies") {
  id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int @col(name: "release_year")
  genre: String @col(dataType: "varchar(20)")
}
```

### User Table with Auth

```graphql
type User @table(key: "uid") {
  uid: String! @default(expr: "auth.uid")
  email: String! @unique
  displayName: String @col(dataType: "varchar(100)")
  createdAt: Timestamp! @default(expr: "request.time")
}
```

______________________________________________________________________

## Core Directives

### @table

Defines a database table.

| Argument   | Description                                |
| ---------- | ------------------------------------------ |
| `name`     | PostgreSQL table name (snake_case default) |
| `key`      | Primary key field(s), default `["id"]`     |
| `singular` | Singular name for generated fields         |
| `plural`   | Plural name for generated fields           |

### @col

Customizes column mapping.

| Argument   | Description                                           |
| ---------- | ----------------------------------------------------- |
| `name`     | Column name in PostgreSQL                             |
| `dataType` | PostgreSQL type: `serial`, `varchar(n)`, `text`, etc. |
| `size`     | Required for `Vector` type                            |

### @default

Sets default value for inserts.

| Argument | Description                                                                                                  |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| `value`  | Literal value: `@default(value: "draft")`                                                                    |
| `expr`   | CEL expression: `@default(expr: "uuidV4()")`, `@default(expr: "auth.uid")`, `@default(expr: "request.time")` |
| `sql`    | Raw SQL: `@default(sql: "now()")`                                                                            |

**Common expressions:**

- `uuidV4()` - Generate UUID
- `auth.uid` - Current user's Firebase Auth UID
- `request.time` - Server timestamp

### @unique

Adds unique constraint.

```graphql
type User @table {
  email: String! @unique
}

# Composite unique
type Review @table @unique(fields: ["movie", "user"]) {
  movie: Movie!
  user: User!
  rating: Int
}
```

### @index

Creates database index for query performance.

```graphql
type Movie @table @index(fields: ["genre", "releaseYear"], order: [ASC, DESC]) {
  title: String! @index
  genre: String
  releaseYear: Int
}
```

| Argument | Description                                                   |
| -------- | ------------------------------------------------------------- |
| `fields` | Fields for composite index (on @table)                        |
| `order`  | `[ASC]` or `[DESC]` for each field                            |
| `type`   | `BTREE` (default), `GIN` (arrays), `HNSW`/`IVFFLAT` (vectors) |

### @searchable

Enables full-text search on String fields.

```graphql
type Post @table {
  title: String! @searchable
  body: String! @searchable(language: "english")
}

# Usage
query SearchPosts($q: String!) @auth(level: PUBLIC) {
  posts_search(query: $q) { id title body }
}
```

______________________________________________________________________

## Relationships

### One-to-Many (Implicit Foreign Key)

```graphql
type Post @table {
  id: UUID! @default(expr: "uuidV4()")
  author: User!  # Creates authorId foreign key
  title: String!
}

type User @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String!
  # Auto-generated: posts_on_author: [Post!]!
}
```

### @ref Directive

Customizes foreign key reference.

```graphql
type Post @table {
  author: User! @ref(fields: "authorId", references: "id")
  authorId: UUID!  # Explicit FK field
}
```

| Argument         | Description                         |
| ---------------- | ----------------------------------- |
| `fields`         | Local FK field name(s)              |
| `references`     | Target field(s) in referenced table |
| `constraintName` | PostgreSQL constraint name          |

**Cascade behavior:**

- Required reference (`User!`): CASCADE DELETE (post deleted when user deleted)
- Optional reference (`User`): SET NULL (authorId set to null when user deleted)

### One-to-One

Use `@unique` on the reference field:

```graphql
type User @table { id: UUID! name: String! }

type UserProfile @table {
  user: User! @unique  # One profile per user
  bio: String
  avatarUrl: String
}

# Query: user.userProfile_on_user
```

### Many-to-Many

Use a join table with composite primary key:

```graphql
type Movie @table { id: UUID! title: String! }
type Actor @table { id: UUID! name: String! }

type MovieActor @table(key: ["movie", "actor"]) {
  movie: Movie!
  actor: Actor!
  role: String!  # Extra data on relationship
}

# Generated fields:
# - movie.actors_via_MovieActor: [Actor!]!
# - actor.movies_via_MovieActor: [Movie!]!
# - movie.movieActors_on_movie: [MovieActor!]!
```

______________________________________________________________________

## Data Types

| GraphQL Type | PostgreSQL Default | Other PostgreSQL Types      |
| ------------ | ------------------ | --------------------------- |
| `String`     | `text`             | `varchar(n)`, `char(n)`     |
| `Int`        | `int4`             | `int2`, `serial`            |
| `Int64`      | `bigint`           | `bigserial`, `numeric`      |
| `Float`      | `float8`           | `float4`, `numeric`         |
| `Boolean`    | `boolean`          |                             |
| `UUID`       | `uuid`             |                             |
| `Date`       | `date`             |                             |
| `Timestamp`  | `timestamptz`      | Stored as UTC               |
| `Any`        | `jsonb`            |                             |
| `Vector`     | `vector`           | Requires `@col(size: N)`    |
| `[Type]`     | Array              | e.g., `[String]` → `text[]` |

______________________________________________________________________

## Enumerations

```graphql
enum Status {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type Post @table {
  status: Status! @default(value: DRAFT)
  allowedStatuses: [Status!]
}
```

**Rules:**

- Enum names: PascalCase, no underscores
- Enum values: UPPER_SNAKE_CASE
- Values are ordered (for comparison operations)
- Changing order or removing values is a breaking change

______________________________________________________________________

## Views (Advanced)

Map custom SQL queries to GraphQL types:

```graphql
type MovieStats @view(sql: """
  SELECT
    movie_id,
    COUNT(*) as review_count,
    AVG(rating) as avg_rating
  FROM review
  GROUP BY movie_id
""") {
  movie: Movie @unique
  reviewCount: Int
  avgRating: Float
}

# Query movies with stats
query TopMovies @auth(level: PUBLIC) {
  movies(orderBy: [{ rating: DESC }]) {
    title
    stats: movieStats_on_movie {
      reviewCount avgRating
    }
  }
}
```
reference/sdk_admin_node.md
# Admin Node SDK

Consult this file when writing server-side code (e.g., Cloud Functions) that
needs elevated privileges or needs to impersonate specific users.

### Best Practices for Agents

- **Understand Operation Storage**: SQL Connect queries and mutations are stored
  on the server like Cloud Functions. Clients do not submit the raw operations.
  Therefore, **whenever you update operations, you must regenerate the SDK and
  redeploy services** that use it.
- **Follow Least Privilege**: Admin SDKs have unrestricted access by default.
  Always use impersonation when possible to limit access.
- **Impersonation**: Use the `impersonate` parameter to run operations as a
  specific user or as an unauthenticated user.
- **Impersonation Variables**: If you call an operation with optional variables
  and want to pass impersonation options but without variables, you **MUST**
  pass `undefined` as the first argument (variables) to clearly indicate no
  variables are being provided.
- **Admin Operations**: If you create operations intended only for
  administration, define them with `@auth(level: NO_ACCESS)`. This ensures they
  can only be called via the Admin SDK with unrestricted access.
- **Resilient Enum Handling**: JavaScript/TypeScript does not enforce exhaustive
  checks on enums. Always add a `default` branch to `switch` statements or an
  `else` branch to handle unknown values gracefully when schemas evolve.

### Configuration in `connector.yaml`

To generate an Admin SDK, add the `adminNodeSdk` block to your `connector.yaml`:

```yaml
connectorId: my-connector
generate:
  adminNodeSdk:
    outputDir: "./admin-sdk"
    package: "@dataconnect/admin-generated"
    packageJsonDir: "." # Directory containing package.json
```

### Generation

Run the generation command:

```bash
npx -y firebase-tools@latest dataconnect:sdk:generate
```

### Usage Examples

#### 1. Impersonating an Unauthenticated User

Unauthenticated users can only run operations marked as `PUBLIC`.

```typescript
import { initializeApp } from "firebase-admin/app";
import { getDataConnect } from "firebase-admin/data-connect";
import { connectorConfig, getSongs } from "@dataconnect/admin-generated";

const adminApp = initializeApp();
const adminDc = getDataConnect(connectorConfig);

const songs = await getSongs(
  adminDc,
  { limit: 4 },
  { impersonate: { unauthenticated: true } }
);
```

#### 2. Impersonating a Specific User (Cloud Functions)

When using callable Cloud Functions, the authentication token is automatically
verified.

```typescript
import { HttpsError, onCall } from "firebase-functions/https";
import { getMyFavoriteSongs } from "@dataconnect/admin-generated";

export const callableExample = onCall(async (req) => {
    const authClaims = req.auth?.token;
    if (!authClaims) {
        throw new HttpsError("unauthenticated", "Unauthorized");
    }

    const favoriteSongs = await getMyFavoriteSongs(
        adminDc,
        undefined,
        { impersonate: { authClaims } }
    );

    return favoriteSongs;
});
```

#### 3. Impersonating a Specific User (Plain HTTP)

For non-callable endpoints, you must verify the token yourself.

```typescript
import { getAuth } from "firebase-admin/auth";
import { onRequest } from "firebase-functions/https";
import { getMyFavoriteSongs } from "@dataconnect/admin-generated";

const auth = getAuth();

export const httpExample = onRequest(async (req, res) => {
    const token = req.header("authorization")?.replace(/^bearer\s+/i, "");
    if (!token) {
        res.sendStatus(401);
        return;
    }
    let authClaims;
    try {
        authClaims = await auth.verifyIdToken(token);
    } catch {
        res.sendStatus(401);
        return;
    }

    const favoriteSongs = await getMyFavoriteSongs(
        adminDc,
        undefined,
        { impersonate: { authClaims } }
    );

    res.send(favoriteSongs);
});
```

#### 4. Running with Unrestricted Access

Omit the `impersonate` parameter to run with full admin access. Only do this for
true administrative tasks.

```typescript
import { upsertSong } from "@dataconnect/admin-generated";

await upsertSong(adminDc, {
  title: "New Song",
  genre: "Rock"
});
```
reference/sdk_android.md
# Android SDK

Consult this file when writing Android application code (Kotlin) that interacts
with the SQL Connect backend.

### Best Practices for Agents

- **Understand Operation Storage**: SQL Connect queries and mutations are stored
  on the server like Cloud Functions. **Whenever you update operations, you must
  regenerate the SDK and redeploy services** that use it to avoid breaking
  clients.
- **Resilient Enum Handling**: The generated SDK forces handling of unknown
  values by wrapping them in `EnumValue`. You must unwrap it into
  `EnumValue.Known` or `EnumValue.Unknown` to handle schema updates gracefully.
- **Flow Behavior**: While you can collect a Flow from a query, note that **this
  Flow is not updated in real-time automatically** by default. It only produces
  a result when a new query result is retrieved using a call to the query's
  `execute()` method.
- **Leverage Coroutines**: Call `.execute()` within a coroutine scope for
  asynchronous operations.

### Dependencies (build.gradle.kts)

Ensure you have the Kotlin Serialization plugin and standard SQL Connect
dependencies:

```kotlin
plugins {
    kotlin("plugin.serialization") version "1.8.22" // Must match Kotlin version
}

dependencies {
    // [AGENT] Fetch the latest available BoM version from https://firebase.google.com/support/release-notes/android before adding this
    implementation(platform("com.google.firebase:firebase-bom:34.12.0"))
    implementation("com.google.firebase:firebase-dataconnect")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1")
}
```

### Initialization

Retrieve the generated connector instance:

```kotlin
import com.google.firebase.dataconnect.generated.MoviesConnector

val connector = MoviesConnector.instance

// For local development with emulator
// Defaults to correct host for Android emulator (10.0.2.2)
connector.dataConnect.useEmulator()
// Or specify a non-default port:
// connector.dataConnect.useEmulator(port = 9999)
```

### Calling Operations

#### Basic Query

```kotlin
val result = connector.listMovies.execute()
result.data.movies.forEach { movie ->
    println(movie.title)
}
```

#### Mutation

```kotlin
val newMovie = connector.createMovie.execute(
    title = "Empire Strikes Back",
    releaseYear = 1980,
    genre = "Sci-Fi",
    rating = 5
)
```

### Resilient Enum Handling

Unwrap the `EnumValue` to handle known and unknown cases safely.

```kotlin
val result = connector.listMovies.execute()

result.data.movies.forEach { movie ->
    when (val aspect = movie.aspectratio) {
        is EnumValue.Known -> println("Known aspect: ${aspect.value.name}")
        is EnumValue.Unknown -> println("Unknown aspect: ${aspect.stringValue}")
    }
}
```

### Client-Side Caching

Enable caching in `connector.yaml` to reduce requests and support offline
scenarios.

```yaml
generate:
  kotlinSdk:
    outputDir: "../android"
    package: "com.google.firebase.dataconnect.generated"
    clientCache:
      maxAge: 5s
      storage: persistent # Default for Android is persistent
```

Use policies in code:

```kotlin
val queryResult = queryRef.execute(QueryRef.FetchPolicy.CACHE_ONLY)
val queryResult = queryRef.execute(QueryRef.FetchPolicy.SERVER_ONLY)
```

### Data Type Mapping Reference

- GraphQL `String` -> Kotlin `String`
- GraphQL `Int` -> Kotlin `Int` (32-bit)
- GraphQL `Float` -> Kotlin `Double` (64-bit)
- GraphQL `Boolean` -> Kotlin `Boolean`
- GraphQL `UUID` -> Kotlin `java.util.UUID`
- GraphQL `Date` -> Kotlin `com.google.firebase.dataconnect.LocalDate`
- GraphQL `Timestamp` -> Kotlin `com.google.firebase.Timestamp`
- GraphQL `Int64` -> Kotlin `Long`
- GraphQL `Any` -> Kotlin `com.google.firebase.dataconnect.AnyValue`
reference/sdk_flutter.md
# Flutter SDK

Consult this file when writing Flutter application code (Dart) that interacts
with the SQL Connect backend.

### Best Practices for Agents

- **Understand Operation Storage**: SQL Connect queries and mutations are stored
  on the server like Cloud Functions. **Whenever you update operations, you must
  regenerate the SDK and redeploy services** that use it to avoid breaking
  clients.
- **Resilient Enum Handling**: The generated SDK forces handling of unknown
  values for enumerations. Client code must unwrap the `EnumValue` object into
  either `Known` or `Unknown` to handle schema updates gracefully.
- **Use Ref for Subscriptions**: Call `.ref()` on operation methods to get a
  `QueryRef` for advanced usage like subscriptions.
- **Builder Pattern for Optionals**: Use the builder pattern for mutations with
  optional fields.

### Installation

```bash
flutter pub add firebase_data_connect
```

### Imports

```dart
import 'package:firebase_data_connect/firebase_data_connect.dart';
// Import generated connector
import 'generated/movies.dart'; 
```

### Initialization

```dart
// For local development with emulator
MoviesConnector.instance.dataConnect.useDataConnectEmulator('127.0.0.1', 9399);
```

### Calling Operations

#### Basic Query

```dart
final response = await MoviesConnector.instance.listMovies().execute();
print(response.data.movies);
```

#### Mutation with Optional Fields (Builder Pattern)

```dart
await MoviesConnector.instance.createMovie(
  title: 'Empire Strikes Back', 
  releaseYear: 1980, 
  genre: 'Sci-Fi' 
).rating(5).execute();
```

### Resilient Enum Handling

When dealing with schema enumerations, use the forced unwrapping pattern to
handle unknown values (e.g., when a new value is added to the backend but client
is old).

```dart
final result = await MoviesConnector.instance.listMovies().execute();

if (result.data != null && result.data!.isNotEmpty) {
  handleEnumValue(result.data![0].aspectratio);
}

void handleEnumValue(EnumValue<AspectRatio> aspectValue) {
  if (aspectValue.value != null) {
    switch(aspectValue.value!) {
      case AspectRatio.ACADEMY:
        print("Academy aspect");
        break;
      case AspectRatio.WIDESCREEN:
        print("Widescreen aspect");
        break;
      // Add other known cases...
    }
  } else {
    print("Unknown aspect ratio detected: ${aspectValue.stringValue}");
  }
}
```

### Client-Side Caching

Enable caching in `connector.yaml` to reduce requests and support offline
scenarios.

```yaml
generate:
  dartSdk: # Or the appropriate block for your project
    outputDir: ../dart/
    package: "dataconnect_generated"
    clientCache:
      maxAge: 5s
      storage: memory # Or persistent for native
```

Use policies in code:

```dart
// Only serve cached values
await queryRef.execute(fetchPolicy: QueryFetchPolicy.cacheOnly);

// Unconditionally fetch fresh values
await queryRef.execute(fetchPolicy: QueryFetchPolicy.serverOnly);
```

### Real-time Subscriptions

```dart
final queryRef = MoviesConnector.instance.getMovieById(id: "<MOVIE_ID>").ref();
final subscription = queryRef.subscribe().listen((result) {
  final movie = result.data.movie;
  if (movie != null) {
    updateUi(movie.title);
  }
});
```

### Data Type Mapping Reference

- GraphQL `Timestamp` -> Dart `firebase_data_connect.Timestamp`
- GraphQL `Int` -> Dart `int`
- GraphQL `Date` -> Dart `DateTime`
- GraphQL `UUID` -> Dart `string`
- GraphQL `Float` -> Dart `double`
- GraphQL `Boolean` -> Dart `bool`
reference/sdk_ios.md
# iOS SDK

Consult this file when writing iOS application code (Swift) that interacts with
the SQL Connect backend.

### Best Practices for Agents

- **Understand Operation Storage**: SQL Connect queries and mutations are stored
  on the server like Cloud Functions. **Whenever you update operations, you must
  regenerate the SDK and redeploy services** that use it to avoid breaking
  clients.
- **Resilient Enum Handling**: The generated SDK forces handling of unknown
  values by adding an `._UNKNOWN` case. Swift enforces exhaustive switch
  statements, so you must handle this case.
- **Observable Macro**: By default, query refs support the `@Observable` macro
  (iOS 17+), making them ideal for binding to SwiftUI views. The bindable query
  results are available in the `data` variable of the query ref.
- **Handle Errors**: Use `try await` with operation execution as they are
  asynchronous and may throw errors.

### Dependencies (Package.swift or SPM)

Configure the generated SDK as a package dependency in Xcode.

### Initialization

Retrieve the generated connector instance:

```swift
import FirebaseCore
import FirebaseDataConnect

// Assuming connector name is 'movies' in connector.yaml
// The connector name is the lower camel case connectorId defined in connector.yaml suffixed with the word 'Connector'
let connector = DataConnect.moviesConnector

// For local development with emulator
// Defaults to 127.0.0.1:9399
connector.useEmulator() 
// Or specify a non-default port:
// connector.useEmulator(port: 9999)
```

### Calling Operations

#### Basic Query

```swift
let result = try await connector.listMovies.execute()
for movie in result.data.movies {
    print(movie.title)
}
```

#### Mutation

```swift
let mutationResult = try await connector.createMovieMutation.execute(
  title: "Empire Strikes Back",
  releaseYear: 1980,
  genre: "Sci-Fi",
  rating: 5
)
```

### Resilient Enum Handling

Handle generated enums exhaustively, including the `._UNKNOWN` case.

```swift
do {
    let result = try await DataConnect.moviesConnector.listMovies.execute()
    if let data = result.data {
        for movie in data.movies {
            switch movie.aspectratio {
                case .ACADEMY: print("academy")
                case .WIDESCREEN: print("widescreen")
                case .ANAMORPHIC: print("anamorphic")
                case ._UNKNOWN(let unknownAspect): print("Unknown: \(unknownAspect)")
            }
        }
    }
} catch {
    // handle error
}
```

### Client-Side Caching

Enable caching in `connector.yaml` to reduce requests, support offline
scenarios, enable realtime support for queries.

```yaml
generate:
  swiftSdk:
    outputDir: "../ios"
    package: "FirebaseDataConnectGenerated"
    clientCache:
      maxAge: 5s
      storage: persistent # Default for iOS is persistent
```

Use cache policies in code:

```swift
try await execute(fetchPolicy: .cacheOnly)
try await execute(fetchPolicy: .serverOnly)
```

### Subscriptions (Realtime)

#### SwiftUI Example

```swift
import Combine
import SwiftUI

struct ListMovieView: View {
    // QueryRef has the Observable attribute, so its properties will
    // automatically trigger updates on changes.
    private var queryRef = connector.listMoviesByGenreQuery.ref(genre: "Sci-Fi")

    // Store the handle to unsubscribe from query updates.
    @State private var querySub: AnyCancellable?

    var body: some View {
        VStack {
            // Use the query results in a View.
            ForEach(queryRef.data?.movies ?? [], id: \.id) { movie in
                    Text(movie.title)
                }
        }
        .onAppear {
            // Subscribe to the query for updates using the Observable macro.
            Task {
                do {
                    querySub = try await queryRef.subscribe().sink { _ in }
                } catch {
                    print("Error subscribing to query: \(error)")
                }
            }
        }
        .onDisappear {
          querySub?.cancel()
        }
    }
}
```

### Data Type Mapping Reference

- GraphQL `UUID` -> Swift `UUID`
- GraphQL `Date` -> Swift `FirebaseDataConnect.LocalDate`
- GraphQL `Timestamp` -> Swift `FirebaseCore.Timestamp`
- GraphQL `Int` -> Swift `Int`
- GraphQL `Float` -> Swift `Double`
- GraphQL `Boolean` -> Swift `Bool`
reference/sdk_web.md
# Web SDK

Consult this file when writing client-side web code (TypeScript/JavaScript) that
interacts with the SQL Connect backend.

### Best Practices for Agents

- **Understand Operation Storage**: SQL Connect queries and mutations are stored
  on the server like Cloud Functions. **Whenever you update operations, you must
  regenerate the SDK and redeploy services** that use it to avoid breaking
  clients.
- **Resilient Enum Handling**: JavaScript/TypeScript does not enforce exhaustive
  checks on enums. Always add a `default` branch to `switch` statements or an
  `else` branch to handle unknown values gracefully when schemas evolve.
- **TanStack Query vs. Native**: You can generate hooks for React/Angular using
  TanStack Query. Choose either TanStack or SQL Connect's built-in real-time and
  caching support, but do not use both in the same project. SQL Connect offers
  normalized caching and remote invalidation.
- **Emulator Connection**: `connectDataConnectEmulator` is only required if
  connecting to the emulator. Otherwise, the generated SDK auto-creates the
  instance.

### Installation

```bash
npm install firebase
firebase init dataconnect:sdk
```

### Initialization

```typescript
import { connectDataConnectEmulator, getDataConnect } from 'firebase/data-connect';
import { connectorConfig } from '@dataconnect/generated';

const dataConnect = getDataConnect(connectorConfig);
// Configure the SDK to use local emulator
connectDataConnectEmulator(dataConnect, 'localhost', 9399);
```

### Calling Operations

#### Using `executeQuery` (Preferred for clarity)

```typescript
import { executeQuery } from 'firebase/data-connect';
import { listMoviesRef } from '@dataconnect/generated';

const ref = listMoviesRef();
const { data } = await executeQuery(ref);
console.log(data.movies);
```

#### Using Action Shortcuts

```typescript
import { listMovies } from '@dataconnect/generated';

listMovies().then(data => showInUI(data));
```

### Resilient Enum Handling

Use a `default` case or check against `Object.values`.

```typescript
import { getOldestMovie } from '@dataconnect/generated';

const queryResult = await getOldestMovie();

if (queryResult.data) {
  const oldestMovieAspectRatio = queryResult.data.originalAspectRatio;
  switch (oldestMovieAspectRatio) {
      case AspectRatio.ACADEMY:
      case AspectRatio.WIDESCREEN:
        console.log('Filmed in Academy or Widescreen!');
        break;
      default:
        // The default case will catch FULLSCREEN, etc.
        console.log('Not filmed in Academy or Widescreen.');
        break;
  }
}
```

### Client-Side Caching

Enable caching in `connector.yaml`:

```yaml
generate:
  javascriptSdk:
    outputDir: ../web/
    package: "@dataconnect/generated"
    clientCache:
      maxAge: 5s
      storage: memory # Only memory is supported on Web
```

Use policies in code:

```typescript
await executeQuery(queryRef, QueryFetchPolicy.CACHE_ONLY);
await executeQuery(queryRef, QueryFetchPolicy.SERVER_ONLY);
```

### Subscriptions (Realtime)

Use `subscribe()` to receive live updates.

#### Web (Vanilla JS)

```typescript
import { subscribe } from 'firebase/data-connect';
import { getMovieByIdRef } from '@dataconnect/generated';

const queryRef = getMovieByIdRef({ id: "<MOVIE_ID>" });

const unsubscribe = subscribe(queryRef, (result) => {
  console.log("Updated result:", result);
});
```

### TanStack Query Support (React)

To use React hooks, re-run `firebase init dataconnect:sdk` after adding React.

#### Usage

```typescript
import { useListAllMovies } from "@dataconnect/generated/react";

function MyComponent() {
  const { isLoading, data, error } = useListAllMovies();
  // handle loading, error, and data
}
```

### Data Type Mapping Reference

- GraphQL `Timestamp` -> TypeScript `string`
- GraphQL `Date` -> TypeScript `string`
- GraphQL `UUID` -> TypeScript `string`
- GraphQL `Int64` -> TypeScript `string`
- GraphQL `Double` -> TypeScript `number`
- GraphQL `Float` -> TypeScript `number`
reference/search.md
# Search Solutions Reference (Vector & Full-Text Search)

Use this reference to design, configure, and implement search capabilities in
SQL Connect. SQL Connect supports three types of search:

1. **Vector Similarity Search (Semantic)**: Best for finding
   conceptually/semantically similar rows (e.g., recommendations, "more like
   this"). Requires Vertex AI.
1. **Full-Text Search (Lexical)**: Best for keyword and phrase search across
   single or multiple columns. Supports lexical stemming.
1. **String Pattern Filters (Exact/Regex)**: Best for simple prefix, exact
   match, or basic wildcard queries (uses standard Postgres indexing).

______________________________________________________________________

## Search Selection Guide

Use this comparative guide to choose the optimal search strategy for the user's
task:

| Feature / Capability | Vector Similarity Search                            | Full-Text Search                               | String Pattern Filters                                 |
| :------------------- | :-------------------------------------------------- | :--------------------------------------------- | :----------------------------------------------------- |
| **Use Case**         | Semantic search, recommendations, RAG pipelines.    | Keyword search, parsing large text fields.     | Exact matches, regular expressions, simple wildcards.  |
| **Engine Support**   | Vertex AI Embeddings + `pgvector` extension.        | Native PostgreSQL full-text engine.            | Native PostgreSQL indexing (`LIKE`, `ILIKE`).          |
| **Matching Style**   | Semantic/concept proximity.                         | Lexical stemming (tenses, root words).         | Exact character sequence.                              |
| **Column Support**   | Single column per query.                            | Multiple columns combined.                     | Multiple columns via standard logical filters (`_or`). |
| **Overhead**         | High (API execution costs & vector column storage). | Medium (generates indices & tsvector columns). | Low (uses standard index / minimal storage).           |

______________________________________________________________________

## 1. Vector Similarity Search (Semantic)

Perform semantic matching by generating vector embeddings representing the
semantic meaning of text.

### Schema Setup

- **Configure Column Dimensions**: Define the column dimension size using the
  `@col(size: X)` directive — SQL Connect requires an explicit size for Vector
  fields to allocate storage.
- **Match Model Specifications**: Ensure the column size matches the output
  dimension of your chosen embedding model (e.g., **768** for Google Vertex AI's
  `textembedding-gecko` models) to prevent runtime type mismatches.

```graphql
type Movie @table {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  description: String
  # Vector field for description embeddings (Vertex AI gecko size is 768)
  descriptionEmbedding: Vector! @col(size: 768)
}
```

### Automatic Embedding Generation (`_embed` server value)

Ensure you use the exact same embedding model across all queries and mutations
on a given vector field — vector embeddings generated from different model
versions are incompatible and will result in poor search relevance or errors.

#### A. Generation on Insert

Use the `${vectorFieldName}_embed` input parameter to automatically generate and
store embeddings on creation.

```graphql
# connector/mutations.gql
mutation CreateMovieWithEmbedding($title: String!, $description: String!) @auth(level: USER) {
  movie_insert(data: {
    title: $title,
    description: $description,
    descriptionEmbedding_embed: {
      model: "textembedding-gecko@003",
      text: $description
    }
  })
}
```

#### B. Generation on Update

```graphql
# connector/mutations.gql
mutation UpdateMovieDescription($id: UUID!, $description: String!) @auth(level: USER) {
  movie_update(
    id: $id,
    data: {
      description: $description,
      descriptionEmbedding_embed: {
        model: "textembedding-gecko@003",
        text: $description
      }
    }
  )
}
```

### Similarity Search Queries

SQL Connect automatically generates a similarity query function for every
`Vector` field in the format: `${pluralType}_${vectorFieldName}_similarity`

#### A. Auto-Embedding Search

Use `compare_embed` to automatically convert the search query string into an
embedding on the fly using Vertex AI.

```graphql
# connector/queries.gql
query SearchMoviesByDescription($query: String!) @auth(level: PUBLIC) {
  movies_descriptionEmbedding_similarity(
    compare_embed: { model: "textembedding-gecko@003", text: $query },
    limit: 5
  ) {
    id
    title
    description
  }
}
```

#### B. Custom Vector Search

Use `compare` to pass raw pre-computed float arrays (cast as a `Vector!`)
directly to the search without calling Vertex AI.

```graphql
# connector/queries.gql
query SearchMoviesByCustomVector($vector: Vector!, $limit: Int!) @auth(level: PUBLIC) {
  movies_descriptionEmbedding_similarity(
    compare: $vector,
    method: L2,
    limit: $limit
  ) {
    id
    title
  }
}
```

### Tuning Vector Proximity

- **Distance Thresholding**: Select the `_metadata { distance }` field to
  evaluate how close the results are, then define a tight threshold using the
  `within` parameter.
- **Distance Metric Gotcha**: `L2` and `COSINE` return different distance
  scales. Re-tune your `within` threshold if you change the `method` parameter,
  as their distance ranges are not compatible.

```graphql
# connector/queries.gql
query SearchMoviesCosineSimilarity($query: String!) @auth(level: PUBLIC) {
  movies_descriptionEmbedding_similarity(
    compare_embed: { model: "textembedding-gecko@003", text: $query },
    method: COSINE,
    within: 0.5, # Maximum distance threshold
    limit: 5
  ) {
    id
    title
    _metadata { distance }
  }
}
```

______________________________________________________________________

## 2. Full-Text Search (Lexical)

Perform fast, stemmed keyword/phrase searches over single or multiple text
columns in your table.

### Schema Setup

To index columns for full-text search, declare the `@searchable` directive on
the string fields inside your table schema.

```graphql
type Movie @table {
  id: UUID! @default(expr: "uuidV4()")
  title: String! @searchable # Default language (English)
  genre: String @searchable
  description: String @searchable(language: "french") # Custom language
  rating: Float
}
```

- **Stemming Language**: By default, parsing uses English stemming. Configure
  custom stemming using `@searchable(language: "languagename")`.
- **Multi-Column Stemming Gotcha**: Ensure all indexed columns use the exact
  same language when searching over multiple columns in a single query —
  PostgreSQL requires matching text search configurations for multi-column
  queries.

______________________________________________________________________

### Full-Text Search Queries

SQL Connect automatically generates a full-text query function for each `@table`
containing `@searchable` fields in the format: `${pluralType}_search`

```graphql
# connector/queries.gql
query SearchMoviesLexical($query: String!) @auth(level: PUBLIC) {
  movies_search(query: $query, limit: 10) {
    id
    title
    genre
    description
  }
}
```

______________________________________________________________________

### Tuning Full-Text Queries

Configuring query arguments optimizes match relevance and search styles.

#### 1. Query Formats (`queryFormat` argument)

Configure the search interpretation using the `queryFormat` parameter:

- **`QUERY` (Default)**: Web-style search (e.g., `inception OR matrix`,
  `-"space-travel"`, quotes for exact matches).
- **`PLAIN`**: Matches all words in the query string in any lexical order (e.g.,
  `"brown dog"` matches `"the dog was brown"`).
- **`PHRASE`**: Matches the exact, contiguous phrase sequence (e.g.,
  `"brown dog"` matches `"the brown dog"`, but NOT `"dog is brown"`).
- **`ADVANCED`**: Allows standard, complex PostgreSQL `tsquery` operators (e.g.
  `inception & (matrix | sci-fi)`).

```graphql
# connector/queries.gql
query SearchMoviesExactPhrase($query: String!) @auth(level: PUBLIC) {
  movies_search(query: $query, queryFormat: PHRASE) {
    id
    title
  }
}
```

#### 2. Relevance Thresholding (`relevanceThreshold` and `_metadata.relevance`)

Results default to sorting by descending relevance rank. Select
`_metadata { relevance }` to inspect match rankings, then set a minimum
`relevanceThreshold` value to prune loose or irrelevant matches.

```graphql
# connector/queries.gql
query SearchMoviesHighRelevance($query: String!, $threshold: Float!) @auth(level: PUBLIC) {
  movies_search(
    query: $query,
    relevanceThreshold: $threshold, # E.g., 0.05
    limit: 5
  ) {
    id
    title
    _metadata {
      relevance
    }
  }
}
```
reference/security.md
# Security Reference

## Contents

- [@auth Directive](#auth-directive)
- [Access Levels](#access-levels)
- [CEL Expressions](#cel-expressions)
- [@check and @redact](#check-and-redact)
- [Authorization Patterns](#authorization-patterns)
- [Anti-Patterns](#anti-patterns)

______________________________________________________________________

## @auth Directive

Every deployable query/mutation must have `@auth`. Without it, operations
default to `NO_ACCESS`.

```graphql
query PublicData @auth(level: PUBLIC) { ... }
query UserData @auth(level: USER) { ... }
query AdminOnly @auth(expr: "auth.token.admin == true") { ... }
```

| Argument         | Description                                        |
| ---------------- | -------------------------------------------------- |
| `level`          | Preset access level                                |
| `expr`           | CEL expression (alternative to level)              |
| `insecureReason` | Suppress deploy warning for PUBLIC/unfiltered USER |

______________________________________________________________________

## Access Levels

| Level                 | Who Can Access                               | CEL Equivalent                                                           |
| --------------------- | -------------------------------------------- | ------------------------------------------------------------------------ |
| `PUBLIC`              | Anyone, authenticated or not                 | `true`                                                                   |
| `USER_ANON`           | Any authenticated user (including anonymous) | `auth.uid != nil`                                                        |
| `USER`                | Authenticated users (excludes anonymous)     | `auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'` |
| `USER_EMAIL_VERIFIED` | Users with verified email                    | `auth.uid != nil && auth.token.email_verified`                           |
| `NO_ACCESS`           | Admin SDK only                               | `false`                                                                  |

> **Important:** Levels like `USER` are starting points. Always add filters or
> expressions to verify the user can access specific data.

______________________________________________________________________

## CEL Expressions

### Available Bindings

| Binding                 | Description                                |
| ----------------------- | ------------------------------------------ |
| `auth.uid`              | Current user's Firebase UID                |
| `auth.token`            | Auth token claims (see below)              |
| `vars`                  | Operation variables (e.g., `vars.movieId`) |
| `request.time`          | Server timestamp                           |
| `request.operationName` | "query" or "mutation"                      |

### auth.token Fields

| Field                       | Description                                 |
| --------------------------- | ------------------------------------------- |
| `email`                     | User's email address                        |
| `email_verified`            | Boolean: email verified                     |
| `phone_number`              | User's phone                                |
| `name`                      | Display name                                |
| `sub`                       | Firebase UID (same as auth.uid)             |
| `firebase.sign_in_provider` | `password`, `google.com`, `anonymous`, etc. |
| `<custom_claim>`            | Custom claims set via Admin SDK             |

### Expression Examples

```graphql
# Check custom claim
@auth(expr: "auth.token.role == 'admin'")

# Check verified email domain
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")

# Check multiple conditions
@auth(expr: "auth.uid != nil && (auth.token.role == 'editor' || auth.token.role == 'admin')")

# Check variable
@auth(expr: "has(vars.status) && vars.status in ['draft', 'published']")
```

### Using eq_expr in Filters

Compare database fields with auth values:

```graphql
query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id title
  }
}

mutation UpdateMyPost($id: UUID!, $title: String!) @auth(level: USER) {
  post_update(
    first: { where: {
      id: { eq: $id },
      authorUid: { eq_expr: "auth.uid" }
    }},
    data: { title: $title }
  )
}
```

______________________________________________________________________

## @check and @redact

Use `@check` to validate data and `@redact` to hide results from client:

### @check

Validates a field value; aborts if check fails.

```graphql
@check(expr: "this != null", message: "Not found")
@check(expr: "this == 'editor'", message: "Must be editor")
@check(expr: "this.exists(p, p.role == 'admin')", message: "No admin found")
```

| Argument   | Description                                  |
| ---------- | -------------------------------------------- |
| `expr`     | CEL expression; `this` = current field value |
| `message`  | Error message if check fails                 |
| `optional` | If `true`, pass when field not present       |

### @redact

Hides field from response (still evaluated for @check):

```graphql
query @redact { ... }  # Query result hidden but @check still runs
```

### Authorization Data Lookup

Check database permissions before allowing mutation:

```graphql
mutation UpdateMovie($id: UUID!, $title: String!) 
  @auth(level: USER) 
  @transaction {
  # Step 1: Check user has permission
  query @redact {
    moviePermission(
      key: { movieId: $id, userId_expr: "auth.uid" }
    ) @check(expr: "this != null", message: "No access to movie") {
      role @check(expr: "this == 'editor'", message: "Must be editor")
    }
  }
  # Step 2: Update if authorized
  movie_update(id: $id, data: { title: $title })
}
```

### Validate Key Exists

```graphql
mutation MustDeleteMovie($id: UUID!) @auth(level: USER) @transaction {
  movie_delete(id: $id) 
    @check(expr: "this != null", message: "Movie not found")
}
```

______________________________________________________________________

## Authorization Patterns

### User-Owned Resources

```graphql
# Create with owner
mutation CreatePost($content: String!) @auth(level: USER) {
  post_insert(data: {
    authorUid_expr: "auth.uid",
    content: $content
  })
}

# Read own data only
query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id content
  }
}

# Update own data only
mutation UpdatePost($id: UUID!, $content: String!) @auth(level: USER) {
  post_update(
    first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}},
    data: { content: $content }
  )
}

# Delete own data only
mutation DeletePost($id: UUID!) @auth(level: USER) {
  post_delete(
    first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}}
  )
}
```

### Role-Based Access

```graphql
# Admin-only query
query AllUsers @auth(expr: "auth.token.admin == true") {
  users { id email name }
}

# Role from database
mutation AdminAction($id: UUID!) @auth(level: USER) @transaction {
  query @redact {
    user(key: { uid_expr: "auth.uid" }) {
      role @check(expr: "this == 'admin'", message: "Admin required")
    }
  }
  # ... admin action
}
```

### Public Data with Filters

```graphql
query PublicPosts @auth(level: PUBLIC) {
  posts(where: {
    visibility: { eq: "public" },
    publishedAt: { lt_expr: "request.time" }
  }) {
    id title content
  }
}
```

### Tiered Access (Pro Content)

```graphql
query ProContent @auth(expr: "auth.token.plan == 'pro'") {
  posts(where: { visibility: { in: ["public", "pro"] }}) {
    id title content
  }
}
```

______________________________________________________________________

## Anti-Patterns

### ❌ Don't Pass User ID as Variable

```graphql
# BAD - any user can pass any userId
query GetUserPosts($userId: String!) @auth(level: USER) {
  posts(where: { authorUid: { eq: $userId }}) { ... }
}

# GOOD - use auth.uid
query GetMyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) { ... }
}
```

### ❌ Don't Use USER Without Filters

```graphql
# BAD - any authenticated user sees all documents
query AllDocs @auth(level: USER) {
  documents { id title content }
}

# GOOD - filter to user's documents
query MyDocs @auth(level: USER) {
  documents(where: { ownerId: { eq_expr: "auth.uid" }}) { ... }
}
```

### ❌ Don't Trust Unverified Email

```graphql
# BAD - email not verified
@auth(expr: "auth.token.email.endsWith('@company.com')")

# GOOD - verify email first
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")
```

### ❌ Don't Use PUBLIC/USER for Prototyping

During development, set operations to `NO_ACCESS` until you implement proper
authorization. Use emulator and VS Code extension for testing.
SKILL.md
---
name: firebase-data-connect
description: Builds and deploys Firebase SQL Connect (aka Firebase Data Connect) backends with PostgreSQL securely. Use when designing schemas with tables and relations, writing authorized queries and mutations, configuring real-time data updates, or generating type-safe SDKs. Use when you need a relational database with Firebase, or when the user mentions SQL Connect or Data Connect.
metadata:
  category: Databases
---

# Firebase SQL Connect

Firebase SQL Connect is a relational database service using Cloud SQL for
PostgreSQL with GraphQL schema, auto-generated queries/mutations, and type-safe
SDKs.

> [!NOTE] **Product Rename**: Firebase Data Connect was renamed to **Firebase
> SQL Connect**. All instructions, references, and examples in this skill
> repository referring to "Data Connect" or "Firebase Data Connect" apply to
> "SQL Connect" and "Firebase SQL Connect" as well.

## Project Structure

```text
dataconnect/
├── dataconnect.yaml      # Service configuration
├── seed_data.gql         # LOCAL ONLY — prototype/test data
├── schema/
│   └── schema.gql        # Data model (types with @table)
└── connector/
    ├── connector.yaml    # Connector config + SDK generation
    ├── queries.gql       # Queries
    └── mutations.gql     # Mutations
```

## Key Tools for Validation

Rely on these two mechanisms to ensure project correctness:

1. **Review GraphQL Schema**: Both user-defined and generated extensions (in
   `.dataconnect/schema/main/`).
1. **Validate Operations**: Run
   `npx -y firebase-tools@latest dataconnect:compile` against the schema.

## Operation Strategies: GraphQL vs. Native SQL

Always default to **Native GraphQL**. **Native SQL lacks type safety** and
bypasses schema-enforced structures. Only use **Native SQL** when the user
explicitly requests it or when the task requires advanced database features.

| Strategy                     | When to use                                                                                                            | Implementation                                                                                                        |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Native GraphQL** (Default) | Almost all use cases. Standard CRUD, basic filtering/sorting, simple relational joins. Requires full type safety.      | Auto-generated fields (`movie_insert`, `movies`). Strong typing and schema enforcement.                               |
| **Native SQL** (Advanced)    | PostgreSQL extensions (e.g., PostGIS), window functions (`RANK()`), complex aggregations, or highly tuned sub-queries. | Raw SQL string literals via `_select`, `_execute`, etc. Requires strict positional parameters (`$1`). No type safety. |

## Development Workflow

Follow this strict workflow to build your application. You **must** read the
linked reference files for each step to understand the syntax and available
features.

### 1. Define Data Model (`schema/schema.gql`)

Define your GraphQL types, tables, and relationships (which map to a Postgres
schema).

> **Read [reference/schema.md](reference/schema.md)** for:
>
> - `@table`, `@col`, `@default`
> - Relationships (`@ref`, one-to-many, many-to-many)
> - Data types (UUID, Vector, JSON, etc.)

### 2. Define Authorized Operations (`connector/queries.gql`, `connector/mutations.gql`)

Write the queries and mutations your client will use, including authorization
logic. SQL Connect is secure by default.

> **Read [reference/operations.md](reference/operations.md)** for:
>
> - **Queries**: Filtering (`where`), Ordering (`orderBy`), Pagination
>   (`limit`/`offset`).
> - **Mutations**: Create (`_insert`), Update (`_update`), Delete (`_delete`).
> - **Upserts**: Use `_upsert` to "insert or update" records (CRITICAL for user
>   profiles).
> - **Transactions**: Use `@transaction` for multi-step atomic operations. Use
>   `_expr: "response.<prevStep>"` to pass data between steps.
>
> **Read [reference/security.md](reference/security.md)** for authorization:
>
> - `@auth(level: ...)` for PUBLIC, USER, or NO_ACCESS.
> - `@check` and `@redact` for row-level security and validation.
>
> **Read [reference/realtime.md](reference/realtime.md)** for real-time
> subscriptions:
>
> - `@refresh` directive for time-based polling and event-driven updates.
> - CEL conditions to scope refresh triggers precisely.
>
> **Read [reference/native_sql.md](reference/native_sql.md)** for Native SQL
> operations:
>
> - Embedding raw SQL with `_select`, `_selectFirst`, `_execute`
> - Strict rules for positional parameters (`$1`, `$2`), quoting, and CTEs
> - Advanced PostgreSQL features (PostGIS, Window Functions)

### 3. Use type-safe SDK in your apps

Generate type-safe code for your client platform.

Configure SDK generation in `connector.yaml`:

```yaml
connectorId: my-connector
generate:
  javascriptSdk:
    outputDir: "../web-app/src/lib/dataconnect"
    package: "@movie-app/dataconnect"
  kotlinSdk:
    outputDir: "../android-app/app/src/main/kotlin/com/example/dataconnect"
    package: "com.example.dataconnect"
  swiftSdk:
    outputDir: "../ios-app/DataConnect"
```

Generate SDKs:

```bash
npx -y firebase-tools@latest dataconnect:sdk:generate
```

For platform-specific instructions on how to use the generated SDKs, read:

- **Web (TypeScript)**: [reference/sdk_web.md](reference/sdk_web.md)
- **Android (Kotlin)**: [reference/sdk_android.md](reference/sdk_android.md)
- **iOS (Swift)**: [reference/sdk_ios.md](reference/sdk_ios.md)
- **Admin (Node.js)**:
  [reference/sdk_admin_node.md](reference/sdk_admin_node.md)
- **Flutter (Dart)**: [reference/sdk_flutter.md](reference/sdk_flutter.md)

______________________________________________________________________

## Feature Capability Map

If you need to implement a specific feature, consult the mapped reference file:

| Feature                         | Reference File                                               | Key Concepts                                       |
| :------------------------------ | :----------------------------------------------------------- | :------------------------------------------------- |
| **Data Modeling**               | [reference/schema.md](reference/schema.md)                   | `@table`, `@unique`, `@index`, Relations           |
| **Vector Search**               | [reference/search.md](reference/search.md)                   | `Vector`, `@col(dataType: "vector")`, embeddings   |
| **Full-Text Search**            | [reference/search.md](reference/search.md)                   | `@searchable`, `movies_search`                     |
| **Upserting Data**              | [reference/operations.md](reference/operations.md)           | `_upsert` mutations                                |
| **Complex Filters**             | [reference/operations.md](reference/operations.md)           | `_or`, `_and`, `_not`, `eq`, `contains`            |
| **Transactions**                | [reference/operations.md](reference/operations.md)           | `@transaction`, `response` binding                 |
| **Environment Config**          | [reference/config.md](reference/config.md)                   | `dataconnect.yaml`, `connector.yaml`               |
| **Realtime Subscriptions**      | [reference/realtime.md](reference/realtime.md)               | `@refresh`, `subscribe()`, auto-refresh            |
| **Cloud Functions Integration** | [reference/cloud_functions.md](reference/cloud_functions.md) | `onMutationExecuted`, triggering events            |
| **Data Seeding & Migrations**   | [reference/data_seeding.md](reference/data_seeding.md)       | `seed_data.gql`, `_insertMany`, Admin SDK bulk     |
| **Starter Templates**           | [templates.md](templates.md)                                 | CRUD, user-owned resources, many-to-many, SDK init |

______________________________________________________________________

## Deployment & CLI

> **Read [reference/config.md](reference/config.md)** for deep dive on
> configuration.

Follow these patterns based on your current task:

### How to initialize SQL Connect in a Firebase project

1. Understand the app idea. Ask clarification questions if unclear.
1. Run `npx -y firebase-tools@latest init dataconnect`.
1. Validate that the app template and generated SDK are setup.

### How to build apps using SQL Connect locally

1. Start the emulator:
   `npx -y firebase-tools@latest emulators:start --only dataconnect`.
1. Write schema and operations.
1. Seed local test data into `seed_data.gql`. Read
   [reference/data_seeding.md](reference/data_seeding.md#local-prototyping-data-seeding).
1. Run `npx -y firebase-tools@latest dataconnect:compile` or
   `npx -y firebase-tools@latest dataconnect:sdk:generate` to validate them.
1. Use the operations in your app and build it.

### How to deploy SQL Connect to Cloud SQL

1. Run `npx -y firebase-tools@latest deploy --only dataconnect`.

## Examples

For complete, working code examples of schemas and operations, see
**[examples.md](examples.md)**.

For ready-to-use starter templates (CRUD, user-owned resources, many-to-many,
YAML configs, SDK init), see **[templates.md](templates.md)**.
templates.md
# Templates

Ready-to-use templates for common Firebase SQL Connect patterns.

______________________________________________________________________

## Basic CRUD Schema

```graphql
# schema.gql
type Item @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String!
  description: String
  createdAt: Timestamp! @default(expr: "request.time")
  updatedAt: Timestamp! @default(expr: "request.time")
}
```

```graphql
# queries.gql
query ListItems @auth(level: PUBLIC) {
  items(orderBy: [{ createdAt: DESC }]) {
    id name description createdAt
  }
}

query GetItem($id: UUID!) @auth(level: PUBLIC) {
  item(id: $id) { id name description createdAt updatedAt }
}
```

```graphql
# mutations.gql
mutation CreateItem($name: String!, $description: String) @auth(level: USER) {
  item_insert(data: { name: $name, description: $description })
}

mutation UpdateItem($id: UUID!, $name: String, $description: String) @auth(level: USER) {
  item_update(id: $id, data: {
    name: $name,
    description: $description,
    updatedAt_expr: "request.time"
  })
}

mutation DeleteItem($id: UUID!) @auth(level: USER) {
  item_delete(id: $id)
}
```

______________________________________________________________________

## User-Owned Resources

```graphql
# schema.gql
type User @table(key: "uid") {
  uid: String! @default(expr: "auth.uid")
  email: String! @unique
  displayName: String
}

type Note @table {
  id: UUID! @default(expr: "uuidV4()")
  owner: User!
  title: String!
  content: String
  createdAt: Timestamp! @default(expr: "request.time")
}
```

```graphql
# queries.gql
query MyNotes @auth(level: USER) {
  notes(
    where: { owner: { uid: { eq_expr: "auth.uid" }}},
    orderBy: [{ createdAt: DESC }]
  ) { id title content createdAt }
}

query GetMyNote($id: UUID!) @auth(level: USER) {
  note(
    first: { where: {
      id: { eq: $id },
      owner: { uid: { eq_expr: "auth.uid" }}
    }}
  ) { id title content }
}
```

```graphql
# mutations.gql
mutation CreateNote($title: String!, $content: String) @auth(level: USER) {
  note_insert(data: {
    owner: { uid_expr: "auth.uid" },
    title: $title,
    content: $content
  })
}

mutation UpdateNote($id: UUID!, $title: String, $content: String) @auth(level: USER) {
  note_update(
    first: { where: { id: { eq: $id }, owner: { uid: { eq_expr: "auth.uid" }}}},
    data: { title: $title, content: $content }
  )
}

mutation DeleteNote($id: UUID!) @auth(level: USER) {
  note_delete(
    first: { where: { id: { eq: $id }, owner: { uid: { eq_expr: "auth.uid" }}}}
  )
}
```

______________________________________________________________________

## Many-to-Many Relationship

```graphql
# schema.gql
type Tag @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String! @unique
}

type Article @table {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  content: String!
}

type ArticleTag @table(key: ["article", "tag"]) {
  article: Article!
  tag: Tag!
}
```

```graphql
# queries.gql
query ArticlesByTag($tagName: String!) @auth(level: PUBLIC) {
  articles(where: {
    articleTags_on_article: { tag: { name: { eq: $tagName }}}
  }) {
    id title
    tags: tags_via_ArticleTag { name }
  }
}

query ArticleWithTags($id: UUID!) @auth(level: PUBLIC) {
  article(id: $id) {
    id title content
    tags: tags_via_ArticleTag { id name }
  }
}
```

```graphql
# mutations.gql
mutation AddTagToArticle($articleId: UUID!, $tagId: UUID!) @auth(level: USER) {
  articleTag_insert(data: {
    article: { id: $articleId },
    tag: { id: $tagId }
  })
}

mutation RemoveTagFromArticle($articleId: UUID!, $tagId: UUID!) @auth(level: USER) {
  articleTag_delete(key: { articleId: $articleId, tagId: $tagId })
}
```

______________________________________________________________________

## dataconnect.yaml Template

```yaml
specVersion: "v1"
serviceId: "my-service"
location: "us-central1"
schema:
  source: "./schema"
  datasource:
    postgresql:
      database: "fdcdb"
      cloudSql:
        instanceId: "my-instance"
connectorDirs: ["./connector"]
```

______________________________________________________________________

## connector.yaml Template

```yaml
connectorId: "default"
generate:
  javascriptSdk:
    outputDir: "../web/src/lib/dataconnect"
    package: "@myapp/dataconnect"
  kotlinSdk:
    outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect"
    package: "com.myapp.dataconnect"
  swiftSdk:
    outputDir: "../ios/MyApp/DataConnect"
  dartSdk:
    outputDir: "../flutter/lib/dataconnect"
    package: myapp_dataconnect
```

______________________________________________________________________

## Firebase Init Commands

```bash
# Initialize SQL Connect in project
npx -y firebase-tools@latest init dataconnect

# Initialize with specific project
npx -y firebase-tools@latest use <project-id>
npx -y firebase-tools@latest init dataconnect

# Start emulator for development
npx -y firebase-tools@latest emulators:start --only dataconnect

# Generate SDKs
npx -y firebase-tools@latest dataconnect:sdk:generate

# Deploy to production
npx -y firebase-tools@latest deploy --only dataconnect
```

______________________________________________________________________

## SDK Initialization (Web)

```typescript
// lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect';
import { connectorConfig } from '@myapp/dataconnect';

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  projectId: "...",
};

export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const dataConnect = getDataConnect(app, connectorConfig);

// Connect to emulator in development
if (import.meta.env.DEV) {
  connectDataConnectEmulator(dataConnect, 'localhost', 9399);
}
```

```typescript
// Example usage
import { listItems, createItem } from '@myapp/dataconnect';

// List items
const { data } = await listItems();
console.log(data.items);

// Create item (requires auth)
await createItem({ name: 'New Item', description: 'Description' });
```

______________________________________________________________________

## Realtime Query Templates

### Time-Based Polling

```graphql
query LiveDashboard
  @auth(level: PUBLIC)
  @refresh(every: { seconds: 30 }) {
  items(orderBy: [{ updatedAt: DESC }], limit: 20) {
    id name updatedAt
  }
}
```

### Event-Driven Refresh

```graphql
query ItemList($categoryId: UUID!)
  @auth(level: PUBLIC)
  @refresh(onMutationExecuted: {
    operation: "CreateItem",
    condition: "request.variables.categoryId == mutation.variables.categoryId"
  }) {
  items(where: { category: { id: { eq: $categoryId }}}) {
    id name createdAt
  }
}
```

### Client Subscribe (Web)

```typescript
import { liveDashboardRef } from '@myapp/dataconnect';
import { subscribe } from 'firebase/data-connect';

const unsubscribe = subscribe(liveDashboardRef(), {
  onNext: (result) => {
    // Called immediately with current data, then on each refresh
    renderDashboard(result.data.items);
  },
  onError: (error) => console.error('Subscription error:', error)
});

// Cleanup when done
// unsubscribe();
```