From 55a8cf221a57e1fc0cd8945851d8aeadf370b324 Mon Sep 17 00:00:00 2001 From: Christopher Rrapi <39552369+crrapi@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:22:46 -0700 Subject: [PATCH] fix(data-connect): correct review upsert example and document SDK/emulator behavior - examples: Review must use a composite (movie, user) primary key so review_upsert has a conflict target; the surrogate-id + @unique version fails to compile. Update the dependent MyReviews/DeleteReview operations. - operations: note that _upsert matches on the primary key, not @unique. - sdk_web: queries default to PREFER_CACHE; pass an options object with SERVER_ONLY for fresh/polled reads, and fix the fetch-policy call shape. - config: offline validation/SDK generation runs through the emulator with a demo project; flag that emulator UUIDs are returned without hyphens; clarify outputDir is relative to connector.yaml. - data_seeding: show how to execute seeds against the emulator via the SDK. --- .../firebase-data-connect-basics/examples.md | 19 ++++++----- .../reference/config.md | 32 +++++++++++++++++-- .../reference/data_seeding.md | 21 ++++++++++++ .../reference/operations.md | 7 ++++ .../reference/sdk_web.md | 18 +++++++++-- 5 files changed, 83 insertions(+), 14 deletions(-) diff --git a/skills/firebase-data-connect-basics/examples.md b/skills/firebase-data-connect-basics/examples.md index b0521e23..fc873624 100644 --- a/skills/firebase-data-connect-basics/examples.md +++ b/skills/firebase-data-connect-basics/examples.md @@ -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! @@ -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 } } } @@ -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" }} }} ) diff --git a/skills/firebase-data-connect-basics/reference/config.md b/skills/firebase-data-connect-basics/reference/config.md index 8de81fac..37b63417 100644 --- a/skills/firebase-data-connect-basics/reference/config.md +++ b/skills/firebase-data-connect-basics/reference/config.md @@ -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 | @@ -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 diff --git a/skills/firebase-data-connect-basics/reference/data_seeding.md b/skills/firebase-data-connect-basics/reference/data_seeding.md index 79d8db33..35c31cd0 100644 --- a/skills/firebase-data-connect-basics/reference/data_seeding.md +++ b/skills/firebase-data-connect-basics/reference/data_seeding.md @@ -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, diff --git a/skills/firebase-data-connect-basics/reference/operations.md b/skills/firebase-data-connect-basics/reference/operations.md index 5b2e5741..e0cfc121 100644 --- a/skills/firebase-data-connect-basics/reference/operations.md +++ b/skills/firebase-data-connect-basics/reference/operations.md @@ -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 diff --git a/skills/firebase-data-connect-basics/reference/sdk_web.md b/skills/firebase-data-connect-basics/reference/sdk_web.md index ce87f61a..08b14d6f 100644 --- a/skills/firebase-data-connect-basics/reference/sdk_web.md +++ b/skills/firebase-data-connect-basics/reference/sdk_web.md @@ -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 }); ``` +> **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.