-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Super early dataconnect-basics skill draft #9822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: launch.skills
Are you sure you want to change the base?
Changes from all commits
1890f07
fc43ff9
869d13f
1535b51
cdf47bf
cdef2a6
2a821b6
c5937b4
766970a
75f3a9a
088855c
078b261
bfe8270
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(...)`). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| # 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 }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| # 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 }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| # <field>_on_<foreign_key_field> 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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description for
USER_EMAIL_VERIFIEDis a bit ambiguous. "potential verified users" could be misinterpreted. It would be clearer to state that it's for users who have a verified email.