{"id":13878277,"url":"https://github.com/gmac/graphql-stitching-ruby","last_synced_at":"2025-04-13T05:39:04.578Z","repository":{"id":65784254,"uuid":"591815654","full_name":"gmac/graphql-stitching-ruby","owner":"gmac","description":"GraphQL Schema Stitching for Ruby","archived":false,"fork":false,"pushed_at":"2024-04-08T11:47:24.000Z","size":2207,"stargazers_count":28,"open_issues_count":6,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-04-14T12:09:04.450Z","etag":null,"topics":["graphql","graphql-federation","graphql-ruby","graphql-stitching","ruby","schema-federation","schema-stitching"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gmac.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-22T00:22:15.000Z","updated_at":"2024-05-11T13:50:54.487Z","dependencies_parsed_at":"2023-12-19T06:48:44.903Z","dependency_job_id":"596c456a-fa90-40f6-b7f9-1786f7416924","html_url":"https://github.com/gmac/graphql-stitching-ruby","commit_stats":{"total_commits":52,"total_committers":4,"mean_commits":13.0,"dds":"0.15384615384615385","last_synced_commit":"185d0dc05a54c3cb7b9455ee4158dcb6e8ee93f9"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-stitching-ruby","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-stitching-ruby/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-stitching-ruby/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmac%2Fgraphql-stitching-ruby/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gmac","download_url":"https://codeload.github.com/gmac/graphql-stitching-ruby/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248670506,"owners_count":21142897,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["graphql","graphql-federation","graphql-ruby","graphql-stitching","ruby","schema-federation","schema-stitching"],"created_at":"2024-08-06T08:01:44.904Z","updated_at":"2025-04-13T05:39:04.326Z","avatar_url":"https://github.com/gmac.png","language":"Ruby","readme":"## GraphQL Stitching for Ruby\n\nGraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire graph of locations to be queried through one combined GraphQL surface area.\n\n![Stitched graph](./docs/images/stitching.png)\n\n**Supports:**\n- All operation types: query, mutation, and [subscription](./docs/subscriptions.md).\n- Merged object and abstract types joining though multiple keys.\n- Shared objects, fields, enums, and inputs across locations.\n- Combining local and remote schemas.\n- [File uploads](./docs/http_executable.md) via multipart forms.\n- Tested with all minor versions of `graphql-ruby`.\n\n**NOT Supported:**\n- Computed fields (ie: federation-style `@requires`).\n- Defer/stream.\n\nThis Ruby implementation is designed as a generic library to join basic spec-compliant GraphQL schemas using their existing types and fields in a [DIY](https://dictionary.cambridge.org/us/dictionary/english/diy) capacity. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is a purely high-throughput federation gateway with managed schema deployments, consider more opinionated frameworks such as [Apollo Federation](https://www.apollographql.com/docs/federation/).\n\n## Getting started\n\nAdd to your Gemfile:\n\n```ruby\ngem \"graphql-stitching\"\n```\n\nRun `bundle install`, then require unless running an autoloading framework (Rails, etc):\n\n```ruby\nrequire \"graphql/stitching\"\n```\n\n## Usage\n\nThe [`Client`](./docs/client.md) component builds a stitched graph wrapped in an executable workflow (with optional query plan caching hooks):\n\n```ruby\nmovies_schema = \u003c\u003c~GRAPHQL\n  type Movie { id: ID! name: String! }\n  type Query { movie(id: ID!): Movie }\nGRAPHQL\n\nshowtimes_schema = \u003c\u003c~GRAPHQL\n  type Showtime { id: ID! time: String! }\n  type Query { showtime(id: ID!): Showtime }\nGRAPHQL\n\nclient = GraphQL::Stitching::Client.new(locations: {\n  movies: {\n    schema: GraphQL::Schema.from_definition(movies_schema),\n    executable: GraphQL::Stitching::HttpExecutable.new(url: \"http://localhost:3000\"),\n  },\n  showtimes: {\n    schema: GraphQL::Schema.from_definition(showtimes_schema),\n    executable: GraphQL::Stitching::HttpExecutable.new(url: \"http://localhost:3001\"),\n  },\n  my_local: {\n    schema: MyLocal::GraphQL::Schema,\n  },\n})\n\nresult = client.execute(\n  query: \"query FetchFromAll($movieId:ID!, $showtimeId:ID!){\n    movie(id:$movieId) { name }\n    showtime(id:$showtimeId): { time }\n    myLocalField\n  }\",\n  variables: { \"movieId\" =\u003e \"1\", \"showtimeId\" =\u003e \"2\" },\n  operation_name: \"FetchFromAll\"\n)\n```\n\nSchemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).\n\nA Client bundles up the component parts of stitching, which are worth familiarizing with:\n\n- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.\n- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.\n- [Request](./docs/request.md) - manages the lifecycle of a stitched GraphQL request.\n- [HttpExecutable](./docs/http_executable.md) - proxies requests to remotes with multipart file upload support.\n\n## Merged types\n\n`Object` and `Interface` types may exist with different fields in different graph locations, and will get merged together in the combined schema.\n\n![Merging types](./docs/images/merging.png)\n\nTo facilitate this, schemas should be designed around **merged type keys** that stitching can cross-reference and fetch across locations using **type resolver queries** (discussed below). For those in an Apollo ecosystem, there's also _limited_ support for merging types though [federation `_entities`](./docs/federation_entities.md).\n\n### Merged type keys\n\nForeign keys in a GraphQL schema frequently look like the `Product.imageId` field here:\n\n```graphql\n# -- Products schema:\n\ntype Product {\n  id: ID!\n  imageId: ID!\n}\n\n# -- Images schema:\n\ntype Image {\n  id: ID!\n  url: String!\n}\n```\n\nHowever, this design does not lend itself to merging types across locations. A simple schema refactor makes this foreign key more expressive as an entity type, and turns the key into an _object_ that will merge with analogous objects in other locations:\n\n```graphql\n# -- Products schema:\n\ntype Product {\n  id: ID!\n  image: Image!\n}\n\ntype Image {\n  id: ID!\n}\n\n# -- Images schema:\n\ntype Image {\n  id: ID!\n  url: String!\n}\n```\n\n### Merged type resolver queries\n\nEach location that provides a unique variant of a type must provide at least one _resolver query_ for accessing it. Type resolvers are root queries identified by a `@stitch` directive:\n\n```graphql\ndirective @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION\n```\n\nThis directive tells stitching how to cross-reference and fetch types from across locations, for example:\n\n```ruby\nproducts_schema = \u003c\u003c~GRAPHQL\n  directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION\n\n  type Product {\n    id: ID!\n    name: String!\n  }\n\n  type Query {\n    product(id: ID!): Product @stitch(key: \"id\")\n  }\nGRAPHQL\n\ncatalog_schema = \u003c\u003c~GRAPHQL\n  directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION\n\n  type Product {\n    id: ID!\n    price: Float!\n  }\n\n  type Query {\n    products(ids: [ID!]!): [Product]! @stitch(key: \"id\")\n  }\nGRAPHQL\n\nclient = GraphQL::Stitching::Client.new(locations: {\n  products: {\n    schema: GraphQL::Schema.from_definition(products_schema),\n    executable:  GraphQL::Stitching::HttpExecutable.new(url: \"http://localhost:3001\"),\n  },\n  catalog: {\n    schema: GraphQL::Schema.from_definition(catalog_schema),\n    executable:  GraphQL::Stitching::HttpExecutable.new(url: \"http://localhost:3002\"),\n  },\n})\n```\n\nFocusing on the `@stitch` directive usage:\n\n```graphql\ntype Product {\n  id: ID!\n  name: String!\n}\ntype Query {\n  product(id: ID!): Product @stitch(key: \"id\")\n}\n```\n\n* The `@stitch` directive marks a root query where the merged type may be accessed. The merged type identity is inferred from the field return. This identifier can also be provided as [static configuration](#sdl-based-schemas).\n* The `key: \"id\"` parameter indicates that an `{ id }` must be selected from prior locations so it can be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#argument-shapes) later).\n\nMerged types must have a resolver query in each of their possible locations. The one exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) that contain no exclusive data, such as foreign keys:\n\n```graphql\ntype Product {\n  id: ID!\n}\n```\n\nThe above type contains nothing but a key field that is available in other locations. Therefore, this variant will never require an inbound request to fetch it, and its resolver query may be omitted from this location.\n\n#### List queries\n\nIt's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a resolver query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.\n\n```graphql\ntype Query {\n  products(ids: [ID!]!): [Product]! @stitch(key: \"id\")\n}\n\n# input:  [\"1\", \"2\", \"3\"]\n# result: [{ id: \"1\" }, null, { id: \"3\" }]\n```\n\nSee [error handling](./docs/mechanics.md#stitched-errors) tips for list queries.\n\n#### Abstract queries\n\nIt's okay for resolver queries to be implemented through abstract types. An abstract query will provide access to all of its possible types by default, each of which must implement the key.\n\n```graphql\ninterface Node {\n  id: ID!\n}\ntype Product implements Node {\n  id: ID!\n  name: String!\n}\ntype Query {\n  nodes(ids: [ID!]!): [Node]! @stitch(key: \"id\")\n}\n```\n\nTo customize which types an abstract query provides and their respective keys, you may extend the `@stitch` directive with a `typeName` constraint. This can be repeated to select multiple types.\n\n```graphql\ndirective @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION\n\ntype Product { sku: ID! }\ntype Order { id: ID! }\ntype Customer { id: ID! } # \u003c\u003c not stitched\nunion Entity = Product | Order | Customer\n\ntype Query {\n  entity(key: ID!): Entity\n    @stitch(key: \"sku\", typeName: \"Product\")\n    @stitch(key: \"id\", typeName: \"Order\")\n}\n```\n\n#### Argument shapes\n\nStitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For custom mappings, the `arguments` option may specify a template of GraphQL arguments that insert key selections:\n\n```graphql\ntype Product {\n  id: ID!\n}\ntype Query {\n  product(byId: ID, bySku: ID): Product\n    @stitch(key: \"id\", arguments: \"byId: $.id\")\n}\n```\n\nKey insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver key, or `__typename`. This syntax allows sending multiple arguments that intermix stitching keys with complex input shapes and other static values:\n\n```graphql\ntype Product {\n  id: ID!\n}\nunion Entity = Product\ninput EntityKey {\n  id: ID!\n  type: String!\n}\nenum EntitySource {\n  DATABASE\n  CACHE\n}\n\ntype Query {\n  entities(keys: [EntityKey!]!, source: EntitySource = DATABASE): [Entity]!\n    @stitch(key: \"id\", arguments: \"keys: { id: $.id, type: $.__typename }, source: CACHE\")\n}\n```\n\nSee [resolver arguments](./docs/type_resolver.md#arguments) for full documentation on shaping input.\n\n#### Composite type keys\n\nResolver keys may make composite selections for multiple key fields and/or nested scopes, for example:\n\n```graphql\ninterface FieldOwner {\n  id: ID!\n}\ntype CustomField {\n  owner: FieldOwner!\n  key: String!\n  value: String\n}\ninput CustomFieldLookup {\n  ownerId: ID!\n  ownerType: String!\n  key: String!\n}\n\ntype Query {\n  customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(\n    key: \"owner { id __typename } key\",\n    arguments: \"lookups: { ownerId: $.owner.id, ownerType: $.owner.__typename, key: $.key }\"\n  )\n}\n```\n\nNote that composite key selections may _not_ be distributed across locations. The complete selection criteria must be available in each location that provides the key.\n\n#### Multiple type keys\n\nA type may exist in multiple locations across the graph using different keys, for example:\n\n```graphql\ntype Product { id:ID! }          # storefronts location\ntype Product { id:ID! sku:ID! }  # products location\ntype Product { sku:ID! }         # catelog location\n```\n\nIn the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides resolver queries for each possible key:\n\n```graphql\ntype Product {\n  id: ID!\n  sku: ID!\n}\ntype Query {\n  productById(id: ID!): Product @stitch(key: \"id\")\n  productBySku(sku: ID!): Product @stitch(key: \"sku\")\n}\n```\n\nThe `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:\n\n```graphql\ntype Product {\n  id: ID!\n  sku: ID!\n}\ntype Query {\n  product(id: ID, sku: ID): Product @stitch(key: \"id\") @stitch(key: \"sku\")\n}\n```\n\n#### Class-based schemas\n\nThe `@stitch` directive can be added to class-based schemas with a directive class:\n\n```ruby\nclass StitchingResolver \u003c GraphQL::Schema::Directive\n  graphql_name \"stitch\"\n  locations FIELD_DEFINITION\n  repeatable true\n  argument :key, String, required: true\n  argument :arguments, String, required: false\nend\n\nclass Query \u003c GraphQL::Schema::Object\n  field :product, Product, null: false do\n    directive StitchingResolver, key: \"id\"\n    argument :id, ID, required: true\n  end\nend\n```\n\nThe `@stitch` directive can be exported from a class-based schema to an SDL string by calling `schema.to_definition`.\n\n#### SDL-based schemas\n\nA clean schema may also have stitching directives applied via static configuration by passing a `stitch` array in [location settings](./docs/composer.md#performing-composition):\n\n```ruby\nsdl_string = \u003c\u003c~GRAPHQL\n  type Product {\n    id: ID!\n    sku: ID!\n  }\n  type Query {\n    productById(id: ID!): Product\n    productBySku(sku: ID!): Product\n  }\nGRAPHQL\n\nsupergraph = GraphQL::Stitching::Composer.new.perform({\n  products:  {\n    schema: GraphQL::Schema.from_definition(sdl_string),\n    executable: -\u003e() { ... },\n    stitch: [\n      { field_name: \"productById\", key: \"id\" },\n      { field_name: \"productBySku\", key: \"sku\", arguments: \"mySku: $.sku\" },\n    ]\n  },\n  # ...\n})\n```\n\n#### Custom directive names\n\nThe library is configured to use a `@stitch` directive by default. You may customize this by setting a new name during initialization:\n\n```ruby\nGraphQL::Stitching.stitch_directive = \"resolver\"\n```\n\n## Executables\n\nAn executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(request, source, variables)` and returns a raw GraphQL response:\n\n```ruby\nclass MyExecutable\n  def call(request, source, variables)\n    # process a GraphQL request...\n    return {\n      \"data\" =\u003e { ... },\n      \"errors\" =\u003e [ ... ],\n    }\n  end\nend\n```\n\nA [Supergraph](./docs/supergraph.md) is composed with executable resources provided for each location. Any location that omits the `executable` option will use the provided `schema` as its default executable:\n\n```ruby\nsupergraph = GraphQL::Stitching::Composer.new.perform({\n  first: {\n    schema: FirstSchema,\n    # executable:^^^^^^ delegates to FirstSchema,\n  },\n  second: {\n    schema: SecondSchema,\n    executable: GraphQL::Stitching::HttpExecutable.new(url: \"http://localhost:3001\", headers: { ... }),\n  },\n  third: {\n    schema: ThirdSchema,\n    executable: MyExecutable.new,\n  },\n  fourth: {\n    schema: FourthSchema,\n    executable: -\u003e(req, query, vars) { ... },\n  },\n})\n```\n\nThe `GraphQL::Stitching::HttpExecutable` class is provided as a simple executable wrapper around `Net::HTTP.post` with [file upload](./docs/http_executable.md#graphql-file-uploads) support. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you must manually assign all executables to a `Supergraph` when rehydrating it from cache ([see docs](./docs/supergraph.md)).\n\n## Batching\n\nThe stitching executor automatically batches subgraph requests so that only one request is made per location per generation of data. This is done using batched queries that combine all data access for a given a location. For example:\n\n```graphql\nquery MyOperation_2($_0_key:[ID!]!, $_1_0_key:ID!, $_1_1_key:ID!, $_1_2_key:ID!) {\n  _0_result: widgets(ids: $_0_key) { ... } # \u003c\u003c 3 Widget\n  _1_0_result: sprocket(id: $_1_0_key) { ... } # \u003c\u003c 1 Sprocket\n  _1_1_result: sprocket(id: $_1_1_key) { ... } # \u003c\u003c 1 Sprocket\n  _1_2_result: sprocket(id: $_1_2_key) { ... } # \u003c\u003c 1 Sprocket\n}\n```\n\nTips:\n\n* List queries (like the `widgets` selection above) are generally preferable as resolver queries because they keep the batched document consistent regardless of set size, and make for smaller documents that parse and validate faster.\n* Assure that root field resolvers across your subgraph implement batching to anticipate cases like the three `sprocket` selections above.\n\nOtherwise, there's no developer intervention necessary (or generally possible) to improve upon data access. Note that multiple generations of data may still force the executor to return to a previous location for more data.\n\n## Concurrency\n\nThe [Executor](./docs/executor.md) component builds atop the Ruby fiber-based implementation of `GraphQL::Dataloader`. Non-blocking concurrency requires setting a fiber scheduler via `Fiber.set_scheduler`, see [graphql-ruby docs](https://graphql-ruby.org/dataloader/nonblocking.html). You may also need to build your own remote clients using corresponding HTTP libraries.\n\n## Additional topics\n\n- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)\n- [Schema composition merge patterns](./docs/composer.md#merge-patterns)\n- [Subscriptions tutorial](./docs/subscriptions.md)\n- [Field selection routing](./docs/mechanics.md#field-selection-routing)\n- [Root selection routing](./docs/mechanics.md#root-selection-routing)\n- [Stitched errors](./docs/mechanics.md#stitched-errors)\n- [Null results](./docs/mechanics.md#null-results)\n\n## Examples\n\nThis repo includes working examples of stitched schemas running across small Rack servers. Clone the repo, `cd` into each example and try running it following its README instructions.\n\n- [Merged types](./examples/merged_types)\n- [File uploads](./examples/file_uploads)\n- [Subscriptions](./examples/subscriptions)\n\n## Tests\n\n```shell\nbundle install\nbundle exec rake test [TEST=path/to/test.rb]\n```\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgmac%2Fgraphql-stitching-ruby","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgmac%2Fgraphql-stitching-ruby","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgmac%2Fgraphql-stitching-ruby/lists"}