graphql-mesh icon indicating copy to clipboard operation
graphql-mesh copied to clipboard

Transform-Replace-Field: Leads to null response (clash of user expectation).

Open D1no opened this issue 2 years ago • 5 comments

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 image

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

D1no avatar Jan 24 '22 16:01 D1no

Experiencing this as well.

Schema is showing the correct types: CleanShot 2022-01-25 at 19 37 19@2x

But it is returning null: CleanShot 2022-01-25 at 19 33 57@2x

.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!]
}

rosswaycaster avatar Jan 26 '22 01:01 rosswaycaster

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.

santino avatar Jan 27 '22 11:01 santino

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 the yaml 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

D1no avatar Jan 27 '22 13:01 D1no

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.

santino avatar Jan 27 '22 15:01 santino

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 the REST 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

D1no avatar Jan 27 '22 17:01 D1no