Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions skills/firebase-data-connect-basics/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ type MovieActor @table(key: ["movie", "actor"]) {
character: String
}

# Reviews (user-owned)
type Review @table @unique(fields: ["movie", "user"]) {
id: UUID! @default(expr: "uuidV4()")
# Reviews (user-owned). The composite (movie, user) PRIMARY KEY enforces one
# review per user per movie AND is the conflict target that `review_upsert`
# matches on. A surrogate `id` PK plus `@unique(fields: ["movie", "user"])` does
# NOT work for upsert: `_upsert` keys on the primary key, so it fails with
# "key id is required in upsert but is not set in data".
type Review @table(key: ["movie", "user"]) {
movie: Movie!
user: User!
rating: Int!
Expand Down Expand Up @@ -101,10 +104,10 @@ query GetMovie($id: UUID!) @auth(level: PUBLIC) {
}
}

# User: Get my reviews
# User: Get my reviews (Review has no surrogate `id` — key is movie + user)
query MyReviews @auth(level: USER) {
reviews(where: { user: { uid: { eq_expr: "auth.uid" }}}) {
id rating text createdAt
rating text createdAt
movie { id title posterUrl }
}
}
Expand Down Expand Up @@ -135,11 +138,11 @@ mutation AddReview($movieId: UUID!, $rating: Int!, $text: String)
})
}

# User: Delete my review
mutation DeleteReview($id: UUID!) @auth(level: USER) {
# User: Delete my review (targeted by the natural key, not a surrogate id)
mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
review_delete(
first: { where: {
id: { eq: $id },
movie: { id: { eq: $movieId }},
user: { uid: { eq_expr: "auth.uid" }}
}}
)
Expand Down
32 changes: 29 additions & 3 deletions skills/firebase-data-connect-basics/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,19 @@ Connector configuration and SDK generation:
connectorId: "default"
generate:
javascriptSdk:
outputDir: "../web/src/lib/dataconnect"
outputDir: "../../web/src/lib/dataconnect"
package: "@myapp/dataconnect"
kotlinSdk:
outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect"
outputDir: "../../android/app/src/main/kotlin/com/myapp/dataconnect"
package: "com.myapp.dataconnect"
swiftSdk:
outputDir: "../ios/MyApp/DataConnect"
outputDir: "../../ios/MyApp/DataConnect"
```

> `outputDir` is resolved **relative to the `connector.yaml` directory** (e.g.
> `dataconnect/connector/`). To target a sibling app at the project root, walk
> up two levels — `outputDir: "../../web/src/dataconnect/generated"`.

### SDK Generation Options

| SDK | Fields |
Expand Down Expand Up @@ -165,6 +169,28 @@ Default ports:
- SQL Connect: `9399`
- PostgreSQL: `9939` (local PostgreSQL instance)

The emulator bundles its own PostgreSQL, so no Cloud SQL, Docker, or Java is
required.

### Offline validation and SDK generation

`dataconnect:compile` and `dataconnect:sdk:generate` authenticate against the
backend and fail offline with `could not find default credentials`. For
credential-free local iteration, run the emulator with a **demo project**: it
compiles the schema and connectors on every reload (surfacing errors in
`dataconnect-debug.log`) and regenerates the configured SDKs automatically.

```bash
firebase emulators:start --only dataconnect --project demo-myapp
```

A `demo-`-prefixed project ID keeps the emulator fully offline.

> **Emulator UUIDs come back without hyphens.** The bundled PostgreSQL returns a
> UUID as 32 hex chars (`eeeeeeee...`), whereas Cloud SQL returns the hyphenated
> form. Don't compare ids against hard-coded literals — round-trip the id values
> the SDK returns.

### Emulator Configuration (firebase.json)

```json
Expand Down
21 changes: 21 additions & 0 deletions skills/firebase-data-connect-basics/reference/data_seeding.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ 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.

#### Running the seed

The Data Connect IDE extension runs `seed_data.gql` via its "Run (local)"
action. From the CLI or CI, seed by calling your generated SDK against the
running emulator — point the SDK at the emulator and invoke your create/seed
mutations:

```js
import { initializeApp } from 'firebase/app';
import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect';
import { connectorConfig, addMovie } from '@myapp/dataconnect';

const dc = getDataConnect(initializeApp({ projectId: 'demo-myapp' }), connectorConfig);
connectDataConnectEmulator(dc, '127.0.0.1', 9399);

await addMovie(dc, { id, title, genre }); // a mutation defined in your connector
```

Mutations are never cached, so this is safe to re-run; use `_upsert` /
`_upsertMany` mutations for idempotent re-seeding.

### Seeding Independent Tables (FK Order)

When executing standard bulk insertions (`_insertMany`) across multiple tables,
Expand Down
7 changes: 7 additions & 0 deletions skills/firebase-data-connect-basics/reference/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ mutation UpsertUser($email: String!, $name: String!) @auth(level: USER) {
}
```

> **`_upsert` matches on the PRIMARY KEY, not on `@unique` constraints.** To
> "insert or update" on a natural or composite key (e.g. one review per movie
> per user), make that key the table's `key` (`@table(key: ["movie", "user"])`)
> instead of adding a surrogate `id` PK plus `@unique`. With a surrogate `id`
> PK, `_upsert` requires `id` in `data` and otherwise fails to compile
> (`key id is required in upsert but is not set in data`).

### Delete

```graphql
Expand Down
18 changes: 15 additions & 3 deletions skills/firebase-data-connect-basics/reference/sdk_web.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,25 @@ generate:
storage: memory # Only memory is supported on Web
```

Use policies in code:
The fetch policy is passed in an options object (the second argument to
`executeQuery` and to the generated action shortcuts):

```typescript
await executeQuery(queryRef, QueryFetchPolicy.CACHE_ONLY);
await executeQuery(queryRef, QueryFetchPolicy.SERVER_ONLY);
import { QueryFetchPolicy } from 'firebase/data-connect';

await executeQuery(queryRef, { fetchPolicy: QueryFetchPolicy.CACHE_ONLY });
await executeQuery(queryRef, { fetchPolicy: QueryFetchPolicy.SERVER_ONLY });
await listMovies(dataConnect, { fetchPolicy: QueryFetchPolicy.SERVER_ONLY });
Comment on lines +106 to +108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The generated action shortcuts (such as listMovies) do not accept an options object (like { fetchPolicy }) as an argument. They only accept a DataConnect instance and/or variables. To customize the fetch policy or other execution options, you must use executeQuery with the query reference.

We should remove the incorrect listMovies example to prevent compilation errors for users following this guide.

Suggested change
await executeQuery(queryRef, { fetchPolicy: QueryFetchPolicy.CACHE_ONLY });
await executeQuery(queryRef, { fetchPolicy: QueryFetchPolicy.SERVER_ONLY });
await listMovies(dataConnect, { fetchPolicy: QueryFetchPolicy.SERVER_ONLY });
await executeQuery(queryRef, { fetchPolicy: QueryFetchPolicy.CACHE_ONLY });
await executeQuery(queryRef, { fetchPolicy: QueryFetchPolicy.SERVER_ONLY });

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept intentionally. The generated query shortcuts do accept ExecuteQueryOptions as the trailing argument — the SDK emits overloads listMovies(dc, options?) and recentReviews(dc, vars?, options?), and passing { fetchPolicy: SERVER_ONLY } to them is verified working on firebase 12.x (it's exactly what turns a stale cached read into a fresh one). Only mutation shortcuts (e.g. addReview) omit the options argument.

```

> **Queries default to `PREFER_CACHE`.** A repeated `executeQuery` (or generated
> action shortcut) can return a cached snapshot instead of hitting the server,
> even with no `clientCache` configured. For always-fresh reads — polling, a
> live feed, or asserting just-written data in tests — pass `SERVER_ONLY`. The
> three policies are `PREFER_CACHE` (default), `CACHE_ONLY`, and `SERVER_ONLY`.
> For push-based realtime, prefer `subscribe()` (below); `SERVER_ONLY` polling
> is the fallback when you are not subscribing.

### Subscriptions (Realtime)

Use `subscribe()` to receive live updates.
Expand Down
Loading