Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested fields issue #154

Closed
gyzerok opened this issue Aug 25, 2015 · 12 comments
Closed

Nested fields issue #154

gyzerok opened this issue Aug 25, 2015 · 12 comments

Comments

@gyzerok
Copy link

gyzerok commented Aug 25, 2015

Consider following code:

Schema:

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'User type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User id',
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User name',
    },
    edges: new GraphQLObjectType({
      name: 'UserEdges',
      description: 'User edges',
      fields: () => ({
        todos: {
          type: new GraphQLList(todoType),
          description: 'User todos',
          args: {
            count: {
              name: 'count',
              type: GraphQLInt,
            },
          },
          resolve: (user, params) => {
            console.log('params', params);
            return findTodo(params);
          },
        },
      }),
    }),
  }),
});

const todoType = new GraphQLObjectType({
  name: 'Todo',
  description: 'Todo type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'Todo id',
    },
    text: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'Todo text',
    },
    createdAt: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'Todo creation date',
    },
  }),
});

Query:

Query {
  viewer {
    id,
    name,
    edges {
      todos(count: 3) {
        id,
        ${TodoItem.getFragment('todo')}
      }
    }
  }
}

In the query result edges field is null. Is it a bug or I do something wrong?

@leebyron
Copy link
Contributor

edges: above points directly to a type rather than to a field definition object which includes "type" and "description" properties.

I'll try to make this a fast fail error so it's easier to catch

@gyzerok
Copy link
Author

gyzerok commented Aug 25, 2015

@leebyron Yeah, I've forgot to specify it here. I've found the problem.

Can you tell me how can I operate with virtual fields in my schema? In this example I do not have edges field on my User, but I'd like to create one. How do I do that?

@gyzerok
Copy link
Author

gyzerok commented Aug 26, 2015

I've done now with following example. Please do not think of edges in terms of Relay. In my code I'd want to store all the relations under the edges virtual field and nothing more.

const userEdge = new GraphQLObjectType({
  name: 'UserEdge',
  fields: () => ({
    todos: {
      type: new GraphQLList(todoType),
      description: 'User todos',
      args: {
        count: {
          name: 'count',
          type: GraphQLInt,
        },
      },
      resolve: (user, params, { rootValue }) => {
        return findTodo(params);
      },
    },
  }),
});

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'User type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User id',
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User name',
    },
    edges: {
      type: new GraphQLList(userEdge),
      resolve: (user, args, info) => {
        // Here I'd like to delegate execution to the edge fields
      },
    },
  }),
});

My query:

Query {
  viewer {
    id,
    name,
    edges {
      todos(count: 3) {
        id,
        text
      }
    }
  }
}

Expected result:

"viewer": {
  "id": 'u-1',
  "name": "user",
  "edges": {
    "todos": [/* array of todos*/]
  }
}

What should I write in the resolve?

@freiksenet
Copy link
Contributor

Just pass what you need in todos as a result of resolve of edges. If you have overridden all children resolves you can use the result of resolve to pass info.

@gyzerok
Copy link
Author

gyzerok commented Aug 26, 2015

@freiksenet like this?

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'User type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User id',
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User name',
    },
    edges: {
      type: new GraphQLList(userEdge),
      resolve: (...args) => {
        return { todos: edgeType.fields().todos(...args) }
      },
    },
  }),
});

@freiksenet
Copy link
Contributor

This is one way, then you actually don't need a resolve in todos field at all.

You should also move count to edges arguments, not the Edge type.

@freiksenet
Copy link
Contributor

You basically can do this stuff in two ways.

  1. Return correct structure and parent and add no resolve to children.
  2. Return data or query needed to resolve children from parent resolve and use it from parent in every of the children's resolve.

@gyzerok
Copy link
Author

gyzerok commented Aug 26, 2015

Thanks to @freiksenet for helping in solving this issue. If anyone cares about the solution it is a follows:

const userEdges = new GraphQLObjectType({
  name: 'UserEdges',
  fields: () => ({
    todos: {
      type: new GraphQLList(todoType),
      description: 'User todos',
      args: {
        count: {
          name: 'count',
          type: GraphQLInt,
        },
      },
      resolve: (user, params, { rootValue }) => {
        return findTodo(params);
      },
    },
  }),
});

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'User type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User id',
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User name',
    },
    edges: {
      type: userEdges,
      resolve: () => ({})
    },
  }),
});

@leebyron This case is about adding virtual field into the schema. I think that GraphQL can provide quite better API for it. The idea here is if we provide plain JS object as a field simply delegate execution to its children.

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'User type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User id',
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User name',
    },
    edges: {
      todos: {
        type: new GraphQLList(todoType),
        description: 'User todos',
        args: {
          count: {
            name: 'count',
            type: GraphQLInt,
          },
        },
        resolve: (user, params, { rootValue }) => {
          return findTodo(params);
        },
      },
    },
  }),
});

@leebyron
Copy link
Contributor

I'm sorry but I don't follow any of this. I'm probably tainted by my understanding of what edges refers to from working with Relay.

You have Edges defined as a list of userEdge and Todos defined as a list of todoType? I'm not sure I follow what Edge refers to here, as a User has a list of them, and whatever they are, they each have a list of Todos?

This case is about adding virtual field into the schema. I think that GraphQL can provide quite better API for it. The idea here is if we provide plain JS object as a field simply delegate execution to its children.

I'm sorry, I don't follow this at all. What does "virtual field" refer to in your parlance? Is edges special, or could the property have been anything? How do we distinguish between this nested object any other framings of the API?

If I reframe your example as:

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'User type',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'User id',
    },
    edges: defineEdges({
      todos: { ... },
    }),
  }),
});

could you explain what you expect defineEdges to do or how it might be implemented?

@gyzerok
Copy link
Author

gyzerok commented Aug 27, 2015

@leebyron Sorry for confusing name. I do not use edges in terms of Relay. In my case edges means collection of relations for the type. I just want to store all the relations for the type in one map but I do not want and/or cant add this on database layer.

From your comment I suppose that you rely on the code in the beginning of that issue. But its not reliable anymore. Please check code in the first half of this comment. Here there is no GraphQLList(userEdges) and I suppose you should get an idea.

@caub
Copy link

caub commented Jan 26, 2018

What is unclear to me is the role of rootValue with those 'computed' fields:

for example, this works fine, as pointed above in this thread

const { graphql, GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLString, GraphQLList, GraphQLSchema } = require('graphql');

const getStuffs = () => Array.from({ length: 50 }, (_, i) => ({ id: `_${i}`, name: `n${i}` }));

const StuffType = new GraphQLObjectType({
	name: 'Stuff',
	description: 'Some stuff',
	fields: () => ({
		id: {
			type: new GraphQLNonNull(GraphQLString),
			description: 'Stuff id',
		},
		name: {
			type: GraphQLString,
			description: 'Stuff name',
		},
		custom: {
			type: GraphQLString,
			description: 'cus',
			resolve: () => 'ok',
		}
	})
});

const QueryType = new GraphQLObjectType({
	name: 'Query',
	description: 'The root of all... queries',
	fields: () => ({
		stuffs: {
			type: new GraphQLList(StuffType),
			description: '..',
			args: {
				first: {
					name: 'first',
					type: GraphQLInt,
				},
			},
			resolve: (o, { first = 20 } = {}) => getStuffs().slice(0, first)
		}
	})
});

const schema = new GraphQLSchema({
	query: QueryType,
});

const gql = (source, variableValues) => graphql({
	schema,
	source,
	variableValues,
});

(async () => {
	const d = await gql(`{
		stuffs(first: 10){
			id
			name
			custom
		}
	}`);
	console.log(d.data.stuffs); // returns arrays of `{ id: '_4', name: 'n4', custom: 'ok' }` ,'s
})();

but using rootValue and buildSchema:

const { graphql, buildSchema } = require('graphql');

const str = `
type Query {
	rand: [Float]
	stuffs(first: Int): [Stuff]
}
type Stuff {
	id: ID!
	name: String
	custom: String
}
`;

const schema = buildSchema(str);

const getStuffs = () => Array.from({ length: 50 }, (_, i) => ({ id: `_${i}`, name: `n${i}` }));

const rootValue = {
	stuffs: ({ first = 20 }) => getStuffs().slice(0, first),
	Stuff: {
		custom: () => 'ok',
	}
};

const gql = (source, variableValues) => graphql({
	schema,
	rootValue,
	source,
	variableValues,
});

(async () => {
	const d = await gql(`{
		stuffs(first: 10){
			id
			name
			custom
		}
	}`);
	console.log(d.data.stuffs); // returns arrays of `{ id: '_4', name: 'n4', custom: null }` ,'s
})();

the computed field custom are nulls, I guess Stuff in the rootValue isn't even invoked, and shouldn't even be here.

It'd be possible to manually merge that computed field in the root query adding a .map(s => ({...s, custom: 'ok'})), but it's not practical with more depth levels

It's what makeExecutableSchema in graphql-tools allows: https://launchpad.graphql.com/553zz3zr79

Basically I would prefer the second approach for defining schemas, but still allow computed fields. Or understand why they are not equivalent

(note: I intentionally omitted custom in getStuffs, the goal was to merge it using graphql resolvers)

@IvanGoncharov
Copy link
Member

IvanGoncharov commented Jan 26, 2018

@caub It's better to use stack overflow for such questions: https://github.com/graphql/graphql-js/blob/master/.github/ISSUE_TEMPLATE.md#questions-regarding-how-to-use-graphql


Is this what makeExecutableSchema in graphql-tools aims to solve?

It uses a different approach to the problem.

the computed field custom are nulls, I guess Stuff in the rootValue isn't even invoked

You return new root value from stuffs and it was missing custom property. Here is a fixed version:

const { graphql, buildSchema } = require('graphql');

const str = `
type Query {
  rand: [Float]
  stuffs(first: Int): [Stuff]
}
type Stuff {
  id: ID!
  name: String
  custom: String
}
`;

const schema = buildSchema(str);

class Stuff {
  constructor(i) {
    this.id = `_${i}`;
    this.name = `n{i}`;
  }

  custom() {
    return 'ok';
  }
}
const getStuffs = () => Array.from({ length: 50 }, (_, i) => new Stuff(i));

const rootValue = {
  rand: () => Array.from({ length: 10 }, () => Math.random()),
  stuffs: ({ first = 20 }) => getStuffs().slice(0, first),
};

const gql = (source, variableValues) => graphql({
  schema,
  rootValue,
  source,
  variableValues,
});

(async () => {
  const d = await gql(`{
    stuffs(first: 10){
      id
      name
      custom
    }
  }`);
  console.log(d.data.stuffs); // returns arrays of `{ id: '_4', name: 'n4', custom: 'ok' }` ,'s
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants