graphql-mesh
graphql-mesh copied to clipboard
Transform-Replace-Field: Leads to null response (clash of user expectation).
Hi, I belief this is a bug.
I am connecting to a json rest API and I want to do this:
query MyQuery {
sleep {
sleep { # <-- Get rid of this level
start
end
score
}
}
}
Problem: Schema is correctly changed, but output is null now
So by follwing the example in the Documentation, my .meshrc.yaml
looks like this:
sources:
- name: SleepAPI
handler:
jsonSchema:
baseUrl: http://localhost:4100/
operations:
- type: Query
field: sleep
path: /sleep
method: GET
responseSchema: json-schemas/sleep_response.json
transforms: #
- replace-field: # Analog from the
replacements: # Documentation
- from: #
type: Query # <- Schema Query
field: sleep #
to: #
type: Sleep_History # <- Replace with
field: sleep # Array of Entries
scope: hoistValue #
REST API Return Sample
{
"sleep": [
{
"start": "2010-01-01T01:00:00+01:00",
"end": "2011-01-01T01:00:00+01:00",
"score": 123
},
{
"start": "2012-01-01T01:00:00+01:00",
"end": "2013-01-01T01:00:00+01:00",
"score": 456
},
{
"start": "2014-01-01T01:00:00+01:00",
"end": "2015-01-01T01:00:00+01:00",
"score": 789
}
]
}
JSON Schema
{
"type": "object",
"title": "Sleep History",
"properties": {
"sleep": {
"title": "History",
"type": "array",
"items": [
{
"type": "object",
"title": "Sleep Entry",
"properties": {
"start": {
"type": "string",
"format": "date-time"
},
"end": {
"type": "string",
"format": "date-time"
},
"score": {
"type": "number"
}
}
}
]
}
}
}
Result
Output in the Console (DEBUG=1)
🕸️ - SleepAPI - Query.sleep: => Returning { sleep: [[Object], [Object], [Object]] }
🕸️ - SleepAPI - Query.sleep: Response is not array but return type is list. Normalizing the response
🕸️ - Execute - MyQuery: Execution done with
{ data: { sleep: null } }
After transform, schema is as expected but the return is null
Reproduction
As Codesandbox: https://codesandbox.io/s/reproduction-graphql-mesh-json-api-transform-e0t4b?file=/.meshrc.yaml
The Reproduction Repository: https://github.com/D1no/reproduction-graphql-mesh-json-api-transform
npm install
npm start
Schema: Before Transform
schema {
query: Query
}
type Query {
sleep: Sleep_History
}
type Sleep_History {
sleep: [Sleep_Entry]
}
type Sleep_Entry {
start: DateTime
end: DateTime
score: Float
}
"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."
scalar DateTime
Schema: After Transform
schema {
query: Query
}
type Query {
sleep: [Sleep_Entry]
}
type Sleep_Entry {
start: DateTime
end: DateTime
score: Float
}
"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar."
scalar DateTime
Originally posted by @D1no in https://github.com/Urigo/graphql-mesh/discussions/3526#discussioncomment-2034113
Experiencing this as well.
Schema is showing the correct types:
But it is returning null
:
.meshrc.yaml
:
- replace-field:
typeDefs: ./customTypeDefs.graphql
replacements:
- from:
type: HomepageShelvesResponseV2YamlResponseShelves
field: items
to:
type: NewHomepageShelvesResponse
field: newItems
customTypeDefs.graphql
:
union ShelfItem = SmallPromotionItem | PopularCategoriesItem
type SmallPromotionItem {
category_name: String
display_description: String
}
type PopularCategoriesItem {
name: String
}
type NewHomepageShelvesResponse {
newItems: [ShelfItem!]
}
Hi @D1no, thank you very much for posting your repository, it is super useful.
I have been debugging the issue and finding out the root cause.
This is happening because json-schema handler, which uses @omnigraph/json-schema
under the hood, understands that your transformed top-level sleep
field should return an array; in your case a list of [Sleep_Entry]
However, the Replace field Transform, expects to transform the response (in your case by hoisting a value) from the original schema to accommodate the desired transformed schema..
To wrap it up, we have a json-schema handler that operates at the resolver level, and a Replace field Transform that operates after the resolver.
What's happening here is that json-schema handler receives an object like { sleep: [ { ... }, { ... }, { ... } ] }
but from the schema it knows that the field should return an Array and not an Object, because the schema has been transformed.
The handler doesn't know anything about the transformation of the schema, it only receives the schema after this has been transformed, because Replace Field transform operates on bare
mode only, and does not currently support wrap
mode).
So the json-schema handler has logic to wrap the resolver response into an array, which means that it transforms the above response to [ { sleep: [ { ... }, { ... }, { ... } ] } ]
Finally, the resolver response is intercepted by the Replace field Transform, but this, instead, knows that the field that you wanted to replace was an object and so it expects to hoist a property from that object. Instead, the response has been wrapped by the handler and so the transform is not able to hoist correctly.
So we end up with a handler that doesn't know anything about schema transformation and has logic to match resolver into an Array as the schema expects, and then a Transform which is is aware that schema before the transformation was of type object and so it expects to hoist a property from that object in order to adapt to the transformed schema.
I hope I managed to make myself clear. The way I see this is that it is not really a bug, because the logic available in the json-schema handler to wrap the response in an array is intended behavior, and the logic to hoist the value from an object in Replace field transform works as expected with objects, but is not meant to work with arrays. Ultimately it is the combination of the two that is creating an issue for your use case.
Having said that, I do have a recommendation for you and this is to create a custom resolver composer that can hoist the value that comes back from the json-schema handler.
All you need to do is to change your transform to this:
transforms:
- replace-field:
replacements:
- from:
type: Query
field: sleep
to:
type: Sleep_History
field: sleep
# scope: hoistValue # no longer needed
composer: ./customComposers.js#sleep
And then create a customComposers.js
file with this content:
module.exports = {
sleep: next => async (root, args, context, info) => {
const sleep = await next(root, args, context, info);
return sleep[0].sleep;
},
};
The usage of Replace field Transform with custom composers is documented here, if you want to know more.
Please let me know how this goes.
Thank you very much @santino. Many rest APIs return the top level query and I already thought about writing some js to clean that up before hand, but wanted to stay in the .yaml
environment. Aka, when graphlql-mesh
gets updated I don't need to worry about self written code.
🐛 Anyway, regarding this issue here: The overall expectation-result feels like a bug to me
- Transform
replace-field
breaks the previously working data<->schema relationship. - Therefore, using that feature applies transforms in a naive way.
- Requiring the understanding of the inner-workings of
graphql-mesh
and breaks the intent to do things inside theyaml environment
.
With the Query Anything, Run Anywhere
tag line I expected maybe too much. By seeing more situations like this down the read, it's probably better for me to just return write the resolver for the rest API into the graphQL server itself. At the end of the day, of few lines of typescript can Query Anything, Run Anywhere
by definition without lots of dependencies 😕 meh
Thank you again @santino
Thanks for your comment. Just to bring some clarity I do contribute to this repository, but I am not part of the team that owns and maintains it, so I cannot speak on their behalf.
I am a fond user of graphql-mesh
and I cannot see how custom use cases to alter and transform a specific schema are related to the mission of this tool.
Query anything means you can query different data sources may those be GraphQL, REST, databases etc and implemented with different languages and specifications (OpenApi, gRPC, SOAP, etc). These can be even stitched and combined into a single GraphQL schema with no effort.
Run anywhere means that you can get the executable GraphQL schema running the way you wish, from the built-in tools that graphql-mesh provides, such as dev server and GraphiQL browser IDE, to your own custom server that you can build with Fastify, Restify, Express etc., to even serverless solutions.
I believe graphql-mesh
does deliver on this mission and it does it efficiently.
A few additional comments.
wanted to stay in the .yaml environment ... when graphlql-mesh gets updated I don't need to worry about self-written code
If you need custom use cases, you do need custom-built code. GraphQL-Mesh is a tool offering a load of useful functionalities, same as other frameworks and libraries used to build web applications do, but I am pretty sure no one builds an application or server just using a configuration file, especially when there are custom requirements. Even if you do not need to write custom code, you cannot expect that when the frameworks/libraries you use are updated you can get the new goodies at no cost to you, there are always changes that might need to be propagated down to your implementation.
Transform replace-field breaks the previously working data<->schema relationship
This is not quite true. Instead, you want to use replace-field transform because your "previous data" schema feels broken to you! In fact, you want to transform that schema into something else. Replace Field transform does alter the schema correctly for you, in fact, when you compare the GraphQL Schema before/after the transform; you do get the alterations that you wanted.
using that feature applies transforms in a naive way
Replace Field transform is powerful and allows you to do a lot, the actual schema alteration is not naive and works as expected.
The hoistValue
scope is the functionality that does not work for your custom use case. As the documentation states, hoistValue
:
intercepts the return value of the resolver to ultimately return only the direct child property that has the same name as the target field
This makes it clear that hoistValue
can only work when your resolvers return objects that have properties, whilst the problem here is that the resolver does not return an object but an array.
Requiring the understanding of the inner-workings
I agree that users cannot know the inners of graphql-mesh, but they're not required to! This is why the community is here to offer help and support. People that do now the inners of the tool can advise on what happens when you face challenges and advice for alternative ways to achieve your goals.
I just wanted to clarify a few things, but having said that, I believe we can do something to help on use cases like yours and I am discussing this with the team. Ultimately you are the one that knows best your project and if you think is better to write the entire implementation on your own without taking advantage of tools like grpahql-mesh, that's totally fine.
I just do feel a bit sad when people decide to walk away because they have a scenario that still allows them to do things but in a slightly less ideal manner. We're here to learn from other people and how they want to use graphql-mesh to continue improving this tool. I'm sure if you end up deciding that this tool can still help you, you will most likely be rewarded with improvements to cover your use case.
First of all: Without Contributors like you, the whole open source ecosystem would be dead. You and all the other maintainers are the life blood that keeps projects like this alive. And that is important! From my personal OSS experience, I know how much ownership one takes to make things work — so please, I want to let you know that I feel with you.
From my try with graphql-mesh
I can only report on my experience and shouldn't be generalized. Whats my scenario:
- Saw a talk by @Urigo on youtube, giving me the impression that
graphql-mesh
automates the consolidation of endpoints into one graphql endpoint for quick data accessibility. - That lead to my expectation:
REST Endpoints -> Some limited configuration possibilities -> GraphQL Endpoint
- My disappointment comes from: The limited configuration features via
.yaml
break theREST Endpoint -> (...) -> GraphQL Endpoint
guarantee. - That means continuing to use
graphql-mesh
requires me to make sure again 😩 that the data-to-graphql integrity is intact
So if I, as a User, need to do that, its much easier to just avoid learning (and be cautious of) the graphql-mesh dependency and do:
- query the REST endpoint
- select what I need
- generate graphql schema from json-schema (i.e. with get-graphql-from-jsonschema
- add it to my resolvers
- done
So the bug here is an issue in expectation: That graphql-mesh is smart enough to make sure it continues working, or warns about it, when you use its configuration capabilities. And that's maybe the reason why people walk away from it. The first impression for me was "things will just work as long as you stay within the configs".
And that is a software architecture / design problem. Not contributor mistake. The "data-to-schema guarantee" functionality is simply not there / not made aware of. Which probably bit @rosswaycaster as well.
Anyway, much love from Berlin Dino