diff --git a/skills/data-connect/SKILL.md b/skills/data-connect/SKILL.md new file mode 100644 index 00000000000..a5a9b02de91 --- /dev/null +++ b/skills/data-connect/SKILL.md @@ -0,0 +1,109 @@ +--- +name: data-connect-basics +description: Comprehensive guide for developing with Firebase Data Connect. Use this skill when users need to (1) Provision a new Data Connect service, (2) Write Data Connect schemas (.gql files with @table), (3) Write queries and mutations, or (4) Generate and use typed SDKs. +--- + +# Firebase Data Connect + +Firebase Data Connect maps GraphQL to Cloud SQL (PostgreSQL), providing typed interactions and local development tools. + +## Project Structure & Configuration + +``` +dataconnect/ +├── dataconnect.yaml # Main service configuration. Required. +├── schema/ +│ └── schema.gql # GraphQL schema with @table definitions. Required. +└── connector/ + ├── connector.yaml # Connector configuration. Required. + ├── queries.gql # Any .GQL files in this directory will be included in the connector. + └── mutations.gql +``` + +### Service Configuration (`dataconnect.yaml`) + +Defines the service, location, and database connection. Replace the values with your own. + +```yaml +specVersion: "v1" +serviceId: "my-service" +location: "us-east4" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "my-project-id:us-east4:my-instance" +connectorDirs: ["./connector"] +``` + +### Connector Configuration (`connector.yaml`) + +Defines the connector ID and SDK generation settings. + +```yaml +connectorId: "my-connector" +generate: + javascriptSdk: + outputDir: "../../js/generated" + package: "@firebasegen/default-connector" +``` + +## Schema Definition (`schema.gql`) + +Data Connect schemas use GraphQL syntax with the `@table` directive to map types to PostgreSQL tables. + +### Key Concepts + +* **@table**: Helper directive to map a type to a table. +* **@col**: Helper directive to customize column definition (e.g., `dataType`, `name`). +* **@default**: Helper directive to set default values (e.g., `expr: "auth.uid"`, `expr: "request.time"`). +* **Relationships**: + * **One-to-Many**: Define a field of the related type in the "Many" side table. + * **One-to-One**: Use `@unique` on the foreign key field. + * **Many-to-Many**: Create a join table with composite keys. + + +## Writing Schemas and Operations + +Follow this iterative workflow to ensure correctness: + +1. **Write Schema**: Define your types in `schema/schema.gql`. +2. **Validate Schema**: Run `firebase dataconnect:compile`. + * Fix any errors reported. + * Repeat until compilation succeeds. +3. **Inspect Generated Types**: Read the contents of `.dataconnect/` to understand the generated type definitions. +4. **Write Operations**: Create queries and mutations in `connector/` (e.g., `queries.gql`). +5. **Validate Operations**: Run `firebase dataconnect:compile`. + * Fix any errors. + * Repeat until compilation succeeds. +6. **Test**: Write unit tests to validate that each operation behaves as expected. + +### Example GQL + +See [schema_example.gql](references/schema_example.gql). + +See [queries_example.gql](references/queries_example.gql) for examples of listing, filtering, and joining data. + +See [mutations_example.gql](references/mutations_example.gql) for examples of creating, updating (upsert), and deleting data securely. + +### Key Directives + +* **@auth(level: ...)**: Controls access level. + * `PUBLIC`: Accessible by anyone (requires `insecureReason`). + * `USER`: Accessible by any authenticated user. + * `USER_EMAIL_VERIFIED`: Accessible by potential verified users. + * `NO_ACCESS`: Admin only (internal use). + * **Note**: You can also use `id_expr: "auth.uid"` in filters/data to restrict access to the specific user. + +## SDK Generation + +Data Connect generates typed SDKs for your client apps (Web, Android, iOS, Dart). + +1. **Configure**: Ensure `connector.yaml` has the `generate` block (as shown above). +2. **Generate**: Run `firebase dataconnect:sdk:generate`. + * Use `--watch` to auto-regenerate on changes. +3. **Use in App**: + * Import the generated connector and operations. + * Call operation functions (e.g., `listMovies()`, `createMovie(...)`). diff --git a/skills/data-connect/references/mutations_example.gql b/skills/data-connect/references/mutations_example.gql new file mode 100644 index 00000000000..1ae8a1926a2 --- /dev/null +++ b/skills/data-connect/references/mutations_example.gql @@ -0,0 +1,33 @@ +# Example mutations for a simple movie app + +# Create a movie based on user input +mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!) +@auth(level: USER_EMAIL_VERIFIED, insecureReason: "Any email verified users can create a new movie.") { + movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl }) +} + +# Upsert (update or insert) a user's username based on their auth.uid +mutation UpsertUser($username: String!) @auth(level: USER) { + # The "auth.uid" server value ensures that users can only register their own user. + user_upsert(data: { id_expr: "auth.uid", username: $username }) +} + +# Add a review for a movie +mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!) +@auth(level: USER) { + review_upsert( + data: { + userId_expr: "auth.uid" + movieId: $movieId + rating: $rating + reviewText: $reviewText + # reviewDate defaults to today in the schema. No need to set it manually. + } + ) +} + +# Logged in user can delete their review for a movie +mutation DeleteReview($movieId: UUID!) @auth(level: USER) { + # The "auth.uid" server value ensures that users can only delete their own reviews. + review_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) +} diff --git a/skills/data-connect/references/queries_example.gql b/skills/data-connect/references/queries_example.gql new file mode 100644 index 00000000000..5cb4add5e90 --- /dev/null +++ b/skills/data-connect/references/queries_example.gql @@ -0,0 +1,79 @@ +# Example queries for a simple movie app. + +# @auth() directives control who can call each operation. +# Anyone should be able to list all movies, so the auth level is set to PUBLIC +query ListMovies @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.") { + movies { + id + title + imageUrl + genre + } +} + +# List all users, only admins should be able to list all users, so we use NO_ACCESS +query ListUsers @auth(level: NO_ACCESS) { + users { + id + username + } +} + +# Logged in users can list all their reviews and movie titles associated with the review +# Since the query uses the uid of the current authenticated user, we set auth level to USER +query ListUserReviews @auth(level: USER) { + user(key: { id_expr: "auth.uid" }) { + id + username + # _on_ makes it easy to grab info from another table + # Here, we use it to grab all the reviews written by the user. + reviews: reviews_on_user { + rating + reviewDate + reviewText + movie { + id + title + } + } + } +} + +# Get movie by id +query GetMovieById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Anyone can get a movie by id.") { + movie(id: $id) { + id + title + imageUrl + genre + metadata: movieMetadata_on_movie { + rating + releaseYear + description + # metadata is valid only if movie exists + } + reviews: reviews_on_movie { + reviewText + reviewDate + rating + user { + id + username + } + } + } +} + +# Search for movies, actors, and reviews +query SearchMovie($titleInput: String, $genre: String) @auth(level: PUBLIC, insecureReason: "Anyone can search for movies.") { + movies( + where: { + _and: [{ genre: { eq: $genre } }, { title: { contains: $titleInput } }] + } + ) { + id + title + genre + imageUrl + } +} diff --git a/skills/data-connect/references/schema_example.gql b/skills/data-connect/references/schema_example.gql new file mode 100644 index 00000000000..9ceb0373545 --- /dev/null +++ b/skills/data-connect/references/schema_example.gql @@ -0,0 +1,52 @@ +# Example schema for simple movie review app + +# User table is keyed by Firebase Auth UID. +type User @table { + # `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert. + id: String! @default(expr: "auth.uid") + username: String! @col(dataType: "varchar(50)") + # The `user: User!` field in the Review table generates the following one-to-many query field. + # reviews_on_user: [Review!]! + # The `Review` join table the following many-to-many query field. + # movies_via_Review: [Movie!]! +} + +# Movie is keyed by a randomly generated UUID. +type Movie @table { + # If you do not pass a 'key' to `@table`, Data Connect automatically adds the following 'id' column. + # Feel free to uncomment and customize it. + # id: UUID! @default(expr: "uuidV4()") + title: String! + imageUrl: String! + genre: String +} + +# MovieMetadata is a metadata attached to a Movie. +# Movie <-> MovieMetadata is a one-to-one relationship +type MovieMetadata @table { + # @unique ensures each Movie can only one MovieMetadata. + movie: Movie! @unique + # The movie field adds the following foreign key field. Feel free to uncomment and customize it. + # movieId: UUID! + rating: Float + releaseYear: Int + description: String +} + +# Reviews is a join table between User and Movie. +# It has a composite primary keys `userUid` and `movieId`. +# A user can leave reviews for many movies. A movie can have reviews from many users. +# User <-> Review is a one-to-many relationship +# Movie <-> Review is a one-to-many relationship +# Movie <-> User is a many-to-many relationship +type Review @table(name: "Reviews", key: ["movie", "user"]) { + user: User! + # The user field adds the following foreign key field. Feel free to uncomment and customize it. + # userUid: String! + movie: Movie! + # The movie field adds the following foreign key field. Feel free to uncomment and customize it. + # movieId: UUID! + rating: Int + reviewText: String + reviewDate: Date! @default(expr: "request.time") +}