Apollo-link-rest: Feature Discussion: Adding automatic typename inference

Created on 26 Jan 2018  ·  22Comments  ·  Source: apollographql/apollo-link-rest

Hi,

After @fbartho's PR (#55) to add typenames to nested objects, I expected it to be able to automatically infer typenames based on the objects' property name. However, this wasn't the case.

After talking with him on Slack, he suggested that I opened an issue so that we can discuss if this would be viable as the property could rarely match the TypeName.

In my use case, this would allow to rapidly connect a GraphQL client to a REST server, while providing only the necessary custom __typenames using @fbartho's implementation.

I did an implementation of this feature and I will open a PR shortly.

enhancement💡 question❔

Most helpful comment

@mpgon & @Rsullivan00 I'd support adding a #### Related Libraries section with a link & a 1-liner description of your libraries to our README.

All 22 comments

Thanks for contributing @sky-franciscogoncalves!! I think this is important to discuss.

My personal concern was that in the GraphQL APIs I saw, only the simplest of query had a matching field-name that could be safely pascal-cased into a __typename. Many of them had alternate conjugations, while almost all mutations simply wouldn't work.

Generally, when implementing the typePatcher I think people were worried about having "dangerous" magic that you might not want -- and then would need to conditionally disable. => Complexity!

I'm still a newbie at GraphQL, however, so I'd love it if @peggyrayzis, @jbaxleyiii, or @sabativi could chime in with some feedback about this feature-idea!

Looking through my own graphql API I dont see many cases where the pascal case of the field matches the type since I would normally do

type Action {
  kind: ActionKind!
  name: String!
}

note for pascal casing to work and not clash with other possible types I would need to call kind -> actionKind which feels weird as you already know you are inside an action

type Action {
  actionKind: ActionKind!
  name: String!
}

I guess you could concat the parent Schema (Action) with the nested fields Pascal cased name (Kind) giving you ActionKind. Although this would prevent sharing types ie

type Schedule {
  start: Float
  end: Float
}
type Task {
  schedule: Schedule!
  name: String!
}
type Job {
  schedule: Schedule!
  name: String!
}

What do you think about overloading the existing annotation so it can be used for nested fields without the path attribute?

query MyQuery {
  action @rest(type: "Action", path: "action/") {
    id
    name
    kind @rest(type: "ActionKind") {
      name
    }
  }
}

I know this means adding it in every time that you use that field in query instead of a single time during initialisation of the rest-link. However IMHO I don't mind a little extra verbosity for the explicitness. Also you are already defining the type of the top level schema so having to define the type of children seems more consistent.

Where as the typePatcher system feels weird to me as you end up defining types in two very different places and it moves this concern too far away from the query.

@cloudkite: if you look at this ticket #48, we discussed the option of an @-directive. My general finding was that it was going to be mostly error prone if users have to add them every time they add & remove fields from their selection-sets.

That said, an @type(…) directive could be mostly additive, so I wouldn’t be too opposed to an implementation of that strategy. — my 2cents.

Trying to infer something the type will never work for all cases because REST has no standard and everyone has their own way of naming endpoints, ressources... So I do not see a bright future for this.
However what I find interesting is the @-directive option that comes again on the table which seems to be easy to setup but there are chances that people will sometimes forget about it so we need a way to at least warn them about this.

As a personal solution, I'm working with schemas for my apollo-link-state/apollo-link-rest setup at my company, and I wrote a tool that watches/parses those files, and code-generates my typePatcher code -- It also calls apollo-codegen to code-generate TypeScript bindings. I'd share it here, but it's not really a friendly script so I don't think it's a generally applicable solution, but it's going to work for my team's needs.

Thanks for your feedback. I'm still very new to GraphQL, so I'm sorry for my lack of knowledge.

In my opinion, the current solution can be confusing for newcomers. As it isn't very clear how the TypePatcher works, nor how the user should use it.

I quite like @cloudkite's solution, as it would allow for better understanding of the queries for anyone reading them. In case we move forward with this solution, I agree with @sabativi that we should let people know if they forget to annotate the queries.

On the other hand, should we do the same thing if we stick with the current solution?

I have also been wondering if we could benefit from binding a schema to the RestLink so that would we could infer the typename from that.

If we had a schema like this:

type Schedule {
  start: Float
  end: Float
}
type Task {
  schedule: Schedule!
  name: String!
}

Then when performing the query, we could verify that the type Task is actually defined in the schema and the nested object schedule would have the typename inferred from the type defined in the schema.

query MyQuery {
  task @rest(type: "Task", path: "task/") {
    name
    schedule {
      start
      end
    }
  }
}

Could something like this work?

I really like the approach where everything you should write is inside a query, for example

query MyQuery {
  action @rest(type: "Action", path: "action/") {
    id
    name
    kind @type(name: "ActionKind") {
      name
    }
  }
}

It is much more clearer for newcomers.

Defining types like you suggest can also be confusing for someone learning graphQL as this is more something that you have to do server side.

I like this conversation

@sabativi, thanks for your input!

I have been playing around with this library and I still feel that we should try to write everything we possible can inside the query. However, there are cases in which it may not be possible.

For instance, if you have a polymorphic nested object where its type is given from a property, you need to have a way of doing complex operations. Therefore, unless we come up with a different solution, the TypePatcher really helps.

Example where the set of attributes might change depending on the type property.:

{
  animals: [
    {
      type: 'cat',
      attributes: {
          ....
      },
    },
    {
      type: 'dog',
      attributes: {
          ....
      },
    },
  ],
}

The problem of embedding everything in your query is you have to repeat yourself everywhere.

Example: Even though a User object might be embedded in N APIs, you have to attach the Type annotations for every subpart of user in every query that has a user. If you have currentUser, buddylist, recommendList, nearbyUsers as queries and user has addresses, linkedAccounts, appData, as sub-models then you have to write 4 x 3 = 12 Type annotations — and that’s assuming each query is only rendered once! With the typePatcher currently implemented you only have to patch once per submodel.

The most damming thing in my mind about the @type annotation is that every user has to copy paste / rewrite the annotation as they experiment with their query and add-remove their typed property selection. This would be a big friction point for using it.

To be clear, I don’t mind if we add the annotation — this would be great for very lightweight REST api injections. — if you only want to wrap one or two little REST endpoints. I would be disappointed if this were the only or recommended way to wrap APIs, however. Since we recommend link-rest as a first step for people migrating full-sized APIs to GraphQL, encouraging this pattern would probably scare a non-trivial number of people away, altogether!

I completely agree with you. We should keep both approaches and, if viable, let them interact.

I have started playing around with a possible implementation to allow this behaviour. However, I'm not sure if I am taking the best approach.

At the moment, it is able to add the typename of every nested object and array (of arrays) of objects that adds the @type(name: "Type") annotation.

Also, if TypePatcher adds a typename to an annotated object, it is replaced by the annotation. We can change this action by skipping it if a typename is already set.

Unfortunately, I wasn't able to provide an implementation that allows for adding an annotated type that is next handled by the TypePatcher. As, if I understood correctly, the TypePatcher acts before I'am to analyse if there are any @type annotations. Which is why we get the replacing behaviour for free.

You can check it here. I have added test cases that show what I mentioned previously.

@fbartho Having multiple queries using the same type can be solved using fragments. You can just specify a single fragment that add's all the @type(name: "Type") annotations and the users can just pull in the fragments.

@pyros2097 using shared fragments to enhance queries with @type is a clever idea I hadn’t considered.

  • How exactly would you go about sharing that fragment so that all queries could use it?
  • What happens when two directives collide?

This is how I intended to do using apollo-link-rest. But it fails is deeply nested types. ex: It throws an error for image { url } saying cannot read __typename for 'url' which the @type directive would solve.

export const UserFragment = gql`
  fragment UserFragment on User {
    id
    first_name
    last_name
    image @type(name: "Image") {
      ...ImageFragment
    }
  }
`;

export const ImageFragment = gql`
  fragment ImageFragment on Image {
    url
    width
    height
  }
`;

export const AdventureFragment = gql`
  fragment AdventureFragment on Adventure {
    id
    name
    user @type(name: "User") {
      ...UserFragment
    }
    cover_photo @type(name: "Image") {
      ...ImageFragment
    }
    created_at
    updated_at
  }
`;

export const GetUserQuery = gql`
  query UserAdventures($page: Int!) {
    user @rest(method: "GET", path: "/api/current", type: "User") {
      id @export(as: "id")
      ...UserFragment
      adventures(page: $page) @rest(method: "GET",  path: "/api/adventures", params: { id: $id }, type: "Adventure") {
        ...AdventureFragment
      }
    }
  }
  ${ImageFragment}
  ${UserFragment}
  ${AdventureFragment}
`;
  • Its easy to share the fragment you just need to declare them separately and inject them into the query you would like to use it in.
  • If 2 directives collide I'm guessing the last directive/fragment with that directive will take precedence. Will need to verify this.

@pyros2097 -- what JSON data are you handing to your response? Your example looks fine, but it shouldn't try to use a ___typename from url if url is just a string?

url is just a string but it gives me a warning for all keys in the image fragment. Also it tells me to use the IntrospectionFragmentMatcher from apollo-inmemory-cache. This could an apollo inmemory cache that could not identify the types for caching and not related to apollo-link-rest.

fragmentMatcher: By default, the InMemoryCache uses a heuristic fragment matcher.
If you are using
fragments on unions and interfaces, you will need to use an IntrospectionFragmentMatcher.
For more
information, please read [our guide to setting up fragment matching for unions & interfaces].

@sky-franciscogoncalves Please feel free to submit your @type() annotation as a PR to this repo so we can discuss it directly without further tangling it in this discussion about "automatic" typename inference.

There has been no action on this thread since February, and we separately opened #72 to implement the manual @type(name: …) directive, so I'm comfortable closing this ticket as done for now. Please re-open if you want to continue discussing techniques for truly "automatic" typename inference -- especially if we can think of a safe, standard way of doing that. -- I think our discussion didn't find that technique.

Hi @fbartho! I know this thread is closed for quite a bit, but previously said, there isn't yet, as far as I could find, a way to easily infer typenames. Although the @type annotation is very nice to make experiments, it doesn't scale. And although the functional typepatcher developed in #55 is a very nice alternative, I found it still a bit too verbose for typing a big API.
I that way, I'd be super interested in opinions about a lib I made for easily type patching a big API. It's called apollo-type-patcher and here's a codesandbox demo

For anyone that finds this thread and is working with a JSON API compliant service, I've forked off JSON API Link to automatically infer types for resources. It also provides some convenient relationship flattening.

@mpgon & @Rsullivan00 I'd support adding a #### Related Libraries section with a link & a 1-liner description of your libraries to our README.

Was this page helpful?
0 / 5 - 0 ratings