This example demonstrates the core techniques for passing field dependencies between subservices, covering most of the topics discussed in the official computed fields documentation.
Computed fields involve selecting data from various services, and then sending that data as input into another service that internally computes a result upon it. If you're familiar with the _entities
query of the Apollo Federation spec, computed fields work pretty much the same way. Computed fields are fairly complex and therefore not a preferred solution for basic needs. However, they can solve some tricky problems involving the directionality of foreign keys.
In general, computed fields are most appropraite when:
- A service holds foreign keys without their associated type information (i.e.: a service knows an ID but doesn't know its type). In these cases, the keys can be sent to a remote service to be resolved into typed objects.
- A service manages a collection of bespoke GraphQL types that are of no concern to the rest of the service architecture. In such cases, it may make sense to encapsulate the service by sending external types in for data rather than releasing its types out into the greater service architecture.
However, computed fields have several distinct disadvantages:
- A subservice with computed fields cannot independently resolve its full schema without input from other services. This means computed fields are defunct holes in a subschema unless accessed through the gateway with dependencies satisfied.
- Computed fields rely on passing complex object keys between services rather than primitive scalar keys. Where a primitive key may be recognized as empty and therefore skip requesting data, complex object keys are always seen as truthy values even if their contents are empty. Thus, the gateway is forced to always request data on their behalf, even for predictably empty results unless manual intervention is taken.
This example demonstrates:
- Configuring computed fields.
- Sending complex inputs to subservices.
- Normalizing subservice deprecations in the gateway.
Related examples:
- See stitching directives SDL to write computed fields as schema directives.
cd computed-fields
yarn install
yarn start
The following service is available for interactive queries:
- Stitched gateway: http://localhost:4000/graphql
For simplicity, all subservices in this example are run locally by the gateway server. You could easily break out any subservice into a standalone remote server following the combining local and remote schemas example.
Visit the stitched gateway and try running the following query:
query {
products(upcs: [1, 2, 3, 4]) {
name
price
category {
name
}
metadata {
__typename
name
...on GeoLocation {
name
lat
lon
}
...on SportsTeam {
location {
name
lat
lon
}
}
...on TelevisionSeries {
season
}
}
}
}
The category
and metadata
associations come from the Metadata service, and are merged with Products service data using computed fields (this configuration can also be written using schema directives):
merge: {
Product: {
fields: {
category: { selectionSet: '{ categoryId }', computed: true },
metadata: { selectionSet: '{ metadataIds }', computed: true },
},
fieldName: '_products',
key: ({ categoryId, metadataIds }) => ({ categoryId, metadataIds }),
argsFromKeys: (keys) => ({ keys }),
}
}
In this pattern, the category
and metadata
fields each specify field-level selection sets that will only be collected from other services when their respective fields are requested. The additional computed
setting isolates these fields into a standalone schema that assures the fields are always requested directly by the gateway with their dependencies provided. Together, the results of these selection sets are built into an object key and sent off to the Metadata service to be resolved into its version of the Product
type.
metadata schema:
type Product {
category: Category # @computed(selectionSet: "{ categoryId }")
metadata: [Metadata] # @computed(selectionSet: "{ metadataIds }")
}
input ProductKey {
categoryId: ID
metadataIds: [ID!]
}
type Query {
_products(keys: [ProductKey!]!): [Product]!
}
The metadata
association is a pretty good candidate for computed fields.
Looking at the way associations are structured, the Products service holds metadata record IDs without any associated type information. Therefore, the Products service must send these untyped IDs over to the Metadata service to be resolved into type objects (this is inherently a shortcoming of the data model—in a perfect solution, association data would be migrated over to the Metadata service where both ID and type information is available).
Even if the Products service had all association data available though, there's still some merit in having the Metadata service internalize the Product
type rather than externalizing all of its bespoke Metadata types. Encapsulation of concerns is a valid factor to weigh against implementation complexity.
The catagory
association is needlessly complex using computed fields. It could just as easily use a basic foreign key pattern in the Products service, which would eliminate the unsightly categoryId
field from its schema:
products schema:
type Product {
...
# categoryId: ID << remove this
category: Category
}
type Category {
id: ID!
}
One of the biggest shortcomings of computed fields is their inconsistency—they cannot be resolved outside of gateway context where their dependencies are satisfied. If you also utilize the subservice as a standalone GraphQL resource, then computed fields appear as defunct holes in its schema.
An imperfect solution to this problem is to deprecate all computed fields in the subservice, and then remove those deprecations in the gateway proxy layer. For example:
metadata schema:
type Product {
category: Category @deprecated(reason: "gateway access only")
metadata: [Metadata] @deprecated(reason: "gateway access only")
}
index.js:
const { stitchSchemas } = require('@graphql-tools/stitch');
const { RemoveObjectFieldDeprecations } = require('@graphql-tools/wrap');
stitchSchemas({
subschemas: [{
schema: require('./services/metadata/schema'),
transforms: [new RemoveObjectFieldDeprecations('gateway access only')],
}]
});
The RemoveObjectFieldDeprecations
transform will un-deprecate these fields within the gateway schema. The subservice-level deprecations at least offer some insight as to why certain fields don't work when accessed directly.