Grackle

repository·main·Indexed 41 days ago

https://github.com/typelevel/grackle

A functional GraphQL server for the Typelevel stack, built using cats, cats-effect, fs2, and http4s. It employs a compiler/interpreter model to map GraphQL schemas to various data sources, including in-memory (via circe or shapeless) and database-backed sources such as Postgres, Oracle, and SQL Server. Version 0.26.0 requires Scala 2.13 or 3.3+.

Tokens
24.2K
Snippets
70
Records
103
Agent score
52%

What's inside Grackle

  1. Map GraphQL types to database tables

    main

    To implement a database-backed model, you must map your GraphQL schema to database tables. Grackle provides integrations for doobie (JDBC) and Skunk (native PostgreSQL).

    Implementation Steps:

    1. Define Columns: For each database column, provide its name and a doobie codec of type Meta. Specify if the value is nullable.
    2. Define Root Fields: Define top-level GraphQL fields as SqlObject mappings.
    3. Define Type Mappings: Map each GraphQL type to its corresponding database columns.
    4. Handle Nested Types with Joins: For nested GraphQL types, use Join(columnA, columnB) to translate the relationship into a SQL join. For example, Join(country.code, city.countrycode) joins the country and city tables where the country code matches the city's country code.
  2. Workaround for GraphQL interfaces across multiple tables

    main

    Grackle's SQL module requires all implementors of a GraphQL interface (or branches of a union) to be represented by rows in a single database table with a discriminator column. If your subtypes are stored in separate tables, Grackle's mapping validator will reject the mapping with the error: "Interface implementors are split across multiple tables" and "All implementors of an interface must map to a single database table."

    To resolve this, use a database view to merge the separate tables into a single virtual table that provides the unified shape Grackle expects.

  3. How Grackle compiler phases work

    main

    Grackle compiles GraphQL queries into terms of a query algebra through a sequence of phases. Each phase transforms the query algebra term, moving it closer to a form that the query interpreter can execute directly.

    Built-in Phases

    Several phases are built-in and always run, including:

    • Elaborators: These substitute variables, apply @skip and @include directives, inline fragments, and rewrite field selections based on a mapping's SelectElaborator.

    Customizing Phases

    Mappings can install custom phases by overriding the compilerPhases method. This is a useful way to enforce global policies on incoming queries.

    The Phase Trait

    A phase must implement the Phase trait from QueryCompiler:

    trait Phase {
      def transform(query: Query): Elab[Query]
    }
    • transform takes the query algebra term from the previous phase and returns a transformed term wrapped in the Elab monad.
    • Elab provides access to the schema, the current context, and fragment definitions.
    • You can abort compilation by returning Elab.failure, which results in the client receiving a GraphQL error response with no data.
    override def compilerPhases: List[QueryCompiler.Phase] =
      super.compilerPhases :+ new MyCustomPhase
  4. Run pre-PR checks

    main

    Before submitting a pull request, run the prePR task in the sbt shell. This task triggers extra checks that are also run in the Continuous Integration (CI) pipeline to ensure your changes meet the project's requirements for testing and binary compatibility.

    sbt:root> prePR
  5. Run the Grackle Star Wars demo

    main

    The Star Wars demo is a http4s-based application packaged as the demo submodule. You can run it from the SBT REPL using sbt-revolver.

    Once running, you can interact with the in-memory and database-backed models using the web-based GraphQL Playground available at: http://localhost:8080/playground.html?endpoint=starwars

    sbt:root> demo/reStart
  6. Limiting query size with QuerySizeValidator

    main

    To protect your GraphQL server from expensive or malicious queries (e.g., deeply nested or extremely broad queries), you can use the QuerySizeValidator phase. This phase rejects queries that exceed a configurable maximum depth or width before they reach the interpreter.

    Configuration

    Add the QuerySizeValidator to your compilerPhases override:

    override def compilerPhases: List[QueryCompiler.Phase] =
      super.compilerPhases :+ new QuerySizeValidator(maxDepth = 5, maxWidth = 5)

    Metrics

    • Depth: The number of nested selection levels in the query.
    • Width: The total number of leaf fields selected.

    Both metrics are computed after fragment spreads have been resolved, meaning queries cannot bypass limits by using fragments.

    Error Messages

    • If depth is exceeded: "Query is too deep: depth is X levels, maximum is Y"
    • If width is exceeded: "Query is too wide"
    • If both are exceeded: "Query is too complex"

    Limitations

    QuerySizeValidator uses syntactic measures based on the query text alone. It does not account for the actual size of the underlying data. For example, a field that returns a list of 1,000 elements only contributes 1 to the width. For more granular cost modeling (e.g., accounting for field cardinalities), you should implement a custom phase.

  7. Compile Grackle sources using sbt

    main

    You can compile the project by starting an interactive sbt shell. Once inside the shell, use the following commands:

    • compile: Compiles sources for the currently selected Scala version.
    • +compile: Compiles code for all enabled Scala versions.
    • Test/compile: Compiles test sources for the currently selected Scala version.
    • +Test/compile: Compiles test sources for all enabled Scala versions.
    % sbt
    # Inside the sbt shell:
    sbt:root> compile
  8. Build Grackle locally

    main

    To build Grackle, you need the following prerequisites installed:

    • JDK 11+
    • sbt
    • Docker (required for running database-backed tests)
    • Node.js (required for running Scala.js tests)

    The project uses the sbt-typelevel plugin for configuration and MiMa (Migration Manager) for binary compatibility checks.

    sbt
  9. Install Grackle via sbt

    main

    To add Grackle to your Scala project, add the core library and any optional backends you require to your build.sbt file. Grackle requires Scala 2.13 or 3.3+.

    Core Dependency

    Always include grackle-core.

    Optional Backends

    • In-memory (JSON): Use grackle-circe for support via circe.
    • In-memory (Generic Scala): Use grackle-generic for support via shapeless.
    • Postgres: Use grackle-doobie-pg (JVM only) or grackle-skunk.
    • Oracle: Use grackle-doobie-oracle (JVM only).
    • SQL Server: Use grackle-doobie-mssql (JVM only).
    // Required: Scala 2.13/3.3+
    libraryDependencies += "org.typelevel" %% "grackle-core" % "0.26.0"
    
    // Optional: support for in-memory Json backend using circe
    libraryDependencies += "org.typelevel" %% "grackle-circe" % "0.26.0"
    
    // Optional: support for in-memory generic Scala backend using shapeless
    libraryDependencies += "org.typelevel" %% "grackle-generic" % "0.26.0"
    
    // Optional: support for Postgres backend via Doobie (JVM only)
    libraryDependencies += "org.typelevel" %% "grackle-doobie-pg" % "0.26.0"
    
    // Optional: support for Postgres backend via Skunk
    libraryDependencies += "org.typelevel" %% "grackle-skunk" % "0.26.0"
    
    // Optional: support for Oracle backend via Doobie (JVM only)
    libraryDependencies += "org.typelevel" %% "grackle-doobie-oracle" % "0.26.0"
    
    // Optional: support for SQL Server backend via Doobie (JVM only)
    libraryDependencies += "org.typelevel" %% "grackle-doobie-mssql" % "0.26.0"
  10. Run the Grackle DB-backed demo

    main

    The Grackle demo is a http4s-based application packaged as the demo submodule. You can run it using sbt-revolver from the SBT REPL. Once running, it hosts both in-memory and database-backed services, along with a GraphQL Playground web client.

    To interact with the database-backed model, navigate to the Playground URL in your browser.

    ```bash
    sbt:root> demo/reStart

    GraphQL Playground URL: http://localhost:8080/playground.html?endpoint=world

  11. Run Grackle tests

    main

    Use the test command in the sbt shell to run the test suite. You can target specific platforms or specific test suites:

    Platform-specific tests:

    • rootJVM/test: Runs tests on the JVM platform.
    • rootJS/test: Runs tests on the Scala.js platform.
    • rootNative/test: Runs tests on the Scala Native platform.

    Running a single test suite: You can run a specific suite using testOnly. For example, to run the ScalarsSuite in the coreJVM project: coreJVM/testOnly compiler.ScalarsSuite

    List all projects: Run the projects task to see all available sub-projects (e.g., coreJVM, skunkJS, doobie, etc.).

    sbt:root> projects
    sbt:root> rootJVM/test
    sbt:root> coreJVM/testOnly compiler.ScalarsSuite