I have been going crazy over this for GraphQL. I have seen a lot of resources referring to this last fieldASTs param for the selectionSet. However, it doesn't seem to be there. I haven't found any solid evidence that it is, but it has been brought up in github issues and tutorials. I am a little confused by this. Is there or is there not a 4th param?
I have also tested the other params to see if I can pull it off of those.
const SomeType = new GraphQLObjectType({
name: 'SomeObject',
fields: () => ({
someItems : {
type: new GraphQLList(SomeCustomType),
resolve: (someItems, params, source, fieldASTs) => {
const projections = getProjection(fieldASTs);
return SomeModel.find({}, projections);
}
}
});
with current version (0.7.0), now it is in the forth argument, third argument is for context.
the following observation from this blog post may help.
http://pcarion.com/2015/09/27/GraphQLResolveInfo/
Welp, I found it. I dove through the changelogs and it was changed to be part of the third param. However, it isn't structured the same way
resolve: (item, params, info, fieldASTs) => {
//used to be
fieldASTs.selectionMap.selection.reduce(someLogic);
//now its simply
fieldASTs.reduce(someLogic);
}
I'm using graphql#0.4.14, and found it here:
info //third argument
.fieldASTs[0]
.selectionSet
.selections
//.reduce...
I guess they are still changing everything, so I've added try/catch to getProjection()
Yes the fourth param comprises of fieldASTs but its hooked under the object as an array
const SomeType = new GraphQLObjectType({
name: 'SomeObject',
fields: () => ({
someItems : {
type: new GraphQLList(SomeCustomType),
resolve: (someItems, params, source, options) => {
const projections = getProjection(options.fieldASTs[0]);
return SomeModel.find({}, projections);
}
}
});
This solved for me.
In express-graphql being graphql v0.8.1 I had to do it like this (note the use of mongoose too):
function getProjection (fieldASTs) {
return fieldASTs.fieldNodes[0].selectionSet.selections.reduce((projections, selection) => {
projections[selection.name.value] = 1;
return projections;
}, {});
}
resolve (root, params, info, fieldASTs) {
let filter = {};
if(params._id){
filter._id = params._id;
}
var projections = getProjection(fieldASTs);
return grocerModel.find(filter).select(projections).exec();
}
I am new to graphql, but this resolved it for me and I think something may have changed in the versions as fieldASTs did not exist on the options (4th param) object, but fieldNodes does that has the necessary child objects to perform the projection.
export default {
type: new GraphQLList(teamType),
args: {
name: {
type: GraphQLString
}
},
resolve(someItems, params, source, options) {
const projection = getProjection(options.fieldNodes[0])
return TeamModel
.find()
.select(projection)
.exec()
}
}
There are 2 competing Open Source libraries to achieve what topic starter was asking for:
graphql-fields-list
graphql-list-fields
They both try to solve the same problem, but the former seem to have some more features, including TypeScript support out of the box.
Related
I'm working on a vue3 project using #vue/apollo-composable and #graphql-codegen.
My index page does a search query. Each result from that query has a tile made on the page. I'm expecting the tile queries will be answered by the cache, but instead, they always miss.
At the page level I do this query:
query getTokens($limit: Int!) {
tokens(limit: $limit) {
...tokenInfo
}
}
Inside of the tile component I execute:
query getToken($id: uuid!){
token(id: $id) {
...tokenInfo
}
}
The fragment looks like this:
fragment tokenInfo on token {
id
name
}
Expectation: The cache would handle 100% of the queries inside the tile components. (I'm hoping to avoid the downfalls of serializing this data to vuex).
Reality: I get n+1 backend calls. I've tried a bunch of permutations including getting rid of the fragment. If I send the getToken call with fetchPolicy: 'cache-only' no data is returned.
The apollo client configuration is very basic:
const cache = new InMemoryCache();
const defaultClient = new ApolloClient({
uri: 'http://localhost:8080/v1/graphql',
cache: cache,
connectToDevTools: true,
});
const app = createApp(App)
.use(Store, StateKey)
.use(router)
.provide(DefaultApolloClient, defaultClient);
I'm also attaching a screenshot of my apollo dev tools. It appears that the cache is in fact getting populated with normalized data:
Any help would be greatly appreciated! :)
I've gotten this worked out thanks to #xadm's comment as well as some feedback I received on the Vue discord. Really my confusion is down to me being new to so many of these tools. Deciding to live on the edge and be a vue3 early adopter (which I love in many ways) made it even easier for me to be confused with the variance in documentation qualities right now.
That said, here is what I've got as a solution.
Problem: The actual problem is that, as configured, Apollo has no way to know that getTokens and getToken return the same type (token).
Solution: The minimum configuration I've found that resolves this is as follows:
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
token(_, { args, toReference }) {
return toReference({
__typename: 'token',
id: args?.id,
});
},
},
},
},
});
However, the feels.... kinda gross to me. Ideally, I'd love to see a way to just point apollo at a copy of my schema, or a schema introspection, and have it figure this out for me. If someone is aware of a better way to do that please let me know.
Better(?) Solution: In the short term here what I feel is a slightly more scalable solution:
type CacheRedirects = Record<string, FieldReadFunction>;
function generateCacheRedirects(types: string[]): CacheRedirects {
const redirects: CacheRedirects = {};
for (const type of types) {
redirects[type] = (_, { args, toReference }) => {
return toReference({
__typename: type,
id: args?.id,
});
};
}
return redirects;
}
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
...generateCacheRedirects(['token']),
},
},
},
});
If anyone has any improvements on these, please add a comment/solution! :)
I am pretty new to Redux but I am getting a good hang of it so far.
I am setting up an actions file and I would need to pass two values to payload.
Please let me know if the question has already been asked elsewhere and I missed it.
Thanks!
kramnic
p.s. below is what I have drafted
export const transferValues = (fromId, toId) => {
return {
type: VALUES_TRANSFER,
payload: { fromId, toId }, //should it be array? not sure about syntax here
};
};
you can also have an action of type { type: string, fromId: number, toId: number } instead of a single "payload". that's a matther of preference I think. you just need to adapt you reducers accordingly. I personally like the above attempt more because it represents single attributes like I have them in my state as well. so I can do like a one-to-one mapping. for example:
// state
export type SessionState = {
+id: number,
+username: string,
+sessionId: string,
}
// reducer here
[...]
case SET_USERNAME:
const setUsernameAction = ((action: any): SetUsernameAction)
return {
...state,
username: setUsernameAction.username,
sessionId: setUsernameAction.sessionId,
}
[...]
// example action
export const setUsername = (username: string, sessionId: string): SetUsernameAction => {
return { type: SET_USERNAME, username, sessionId }
}
but you will certainly never use an array for multiple attributes. you'll lose the named access to properties as well as any chance to type your action. stick with objects, no matther what solution you'll go for.
Is there a way to catch and modify response globally on the fly? I can do this for one query like below, but I want to do it for all queries.
apollo: {
post: {
query: Post,
update(data) {
return data.map(item => Object.assign(item, {foo: 'bar'})
}
}
}
It's simplified for this question, but under the hood I'd like to apply a constructor (class) to all objects...
I'm using nuxt-apollo. I searched for a way to do that in clientConfig or elsewhere so the solution may be related to apollo...
Thanks for your advice!
edit:
OK, I found to do that with apollo-link, but I can't modify response. Here the code:
const constructorMiddleware = new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
Object.keys(response.data).map(key => {
if (!Array.isArray(response.data[key])) return;
const newResponse = response.data[key].map(item => {
return item.__typename === 'post'
? Object.assign(item, { foo: 'bar' })
: item
})
console.log(newResponse)
response.data[key] = newResponse
})
return response
})
})
I can see foo: bar in the newResponse, but the graphql returning by nuxt-apollo doesn't contains this newResponse, only original.
Do ApolloLink override response? Does apollo cache change this?
edit 2:
I tried to chain links and the newResponse of the constructorMiddleware is well in the next link. So the problem seems come from nuxt-apollo, or more vue-apollo...
(Currently not using Apollo)
So I have server.js:
let graphqlHTTP = require('express-graphql');
... // (other graphQL setup)
let users = []; // or any DB solution here
const {
UserType,
UserFactory
} = require('./schema/typeDef.js');
const { QueryType } = require('./schema/query.js');
... // similar imports
// other server declarations
And for example, I have typeDef.js:
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLInt) },
username: { type: new GraphQLNonNull(GraphQLString }
}
});
const UserFactory = function(username, id) {
return {username: username, id: id};
};
module.exports = { UserType, UserFactory };
The problem is, I want to be able to add the UserType to the DB (in this code example, the users array) in server.js; but of course, typeDef.js does not have access to the declared DB in server.js.
I initially separated the files because I didn't want server.js to be too bloated with schema code.
How should I go about this? Thanks!
If you've come to the sacred land with no answer, I've got one for you. Follow the guide in this link.
Essentially, create a db with mongoose, export it, and use index.js in different folders to stitch them together. Resolvers will be in a different folder than types, each with their own index.js.
You can now access db methods in resolvers by having it require the db module. Note that you do not need babel, and I've found that cors causes problems (so you may want to remove that if it's causing you problem too).
I've followed the documentation about using graphql-tools to mock a GraphQL server, however this throws an error for custom types, such as:
Expected a value of type "JSON" but received: [object Object]
The graphql-tools documentation about mocking explicitly states that they support custom types, and even provide an example of using the GraphQLJSON custom type from the graphql-type-json project.
I've provided a demo of a solution on github which uses graphql-tools to successfully mock a GraphQL server, but this relies on monkey-patching the built schema:
// Here we Monkey-patch the schema, as otherwise it will fall back
// to the default serialize which simply returns null.
schema._typeMap.JSON._scalarConfig.serialize = () => {
return { result: 'mocking JSON monkey-patched' }
}
schema._typeMap.MyCustomScalar._scalarConfig.serialize = () => {
return mocks.MyCustomScalar()
}
Possibly I'm doing something wrong in my demo, but without the monkey-patched code above I get the error regarding custom types mentioned above.
Does anyone have a better solution than my demo, or any clues as to what I might be doing wrong, and how I can change the code so that the demo works without monkey-patching the schema?
The relevant code in the demo index.js is as follows:
/*
** As per:
** http://dev.apollodata.com/tools/graphql-tools/mocking.html
** Note that there are references on the web to graphql-tools.mockServer,
** but these seem to be out of date.
*/
const { graphql, GraphQLScalarType } = require('graphql');
const { makeExecutableSchema, addMockFunctionsToSchema } = require('graphql-tools');
const GraphQLJSON = require('graphql-type-json');
const myCustomScalarType = new GraphQLScalarType({
name: 'MyCustomScalar',
description: 'Description of my custom scalar type',
serialize(value) {
let result;
// Implement your own behavior here by setting the 'result' variable
result = value || "I am the results of myCustomScalarType.serialize";
return result;
},
parseValue(value) {
let result;
// Implement your own behavior here by setting the 'result' variable
result = value || "I am the results of myCustomScalarType.parseValue";
return result;
},
parseLiteral(ast) {
switch (ast.kind) {
// Implement your own behavior here by returning what suits your needs
// depending on ast.kind
}
}
});
const schemaString = `
scalar MyCustomScalar
scalar JSON
type Foo {
aField: MyCustomScalar
bField: JSON
cField: String
}
type Query {
foo: Foo
}
`;
const resolverFunctions = {
Query: {
foo: {
aField: () => {
return 'I am the result of resolverFunctions.Query.foo.aField'
},
bField: () => ({ result: 'of resolverFunctions.Query.foo.bField' }),
cField: () => {
return 'I am the result of resolverFunctions.Query.foo.cField'
}
},
},
};
const mocks = {
Foo: () => ({
// aField: () => mocks.MyCustomScalar(),
// bField: () => ({ result: 'of mocks.foo.bField' }),
cField: () => {
return 'I am the result of mocks.foo.cField'
}
}),
cField: () => {
return 'mocking cField'
},
MyCustomScalar: () => {
return 'mocking MyCustomScalar'
},
JSON: () => {
return { result: 'mocking JSON'}
}
}
const query = `
{
foo {
aField
bField
cField
}
}
`;
const schema = makeExecutableSchema({
typeDefs: schemaString,
resolvers: resolverFunctions
})
addMockFunctionsToSchema({
schema,
mocks
});
// Here we Monkey-patch the schema, as otherwise it will fall back
// to the default serialize which simply returns null.
schema._typeMap.JSON._scalarConfig.serialize = () => {
return { result: 'mocking JSON monkey-patched' }
}
schema._typeMap.MyCustomScalar._scalarConfig.serialize = () => {
return mocks.MyCustomScalar()
}
graphql(schema, query).then((result) => console.log('Got result', JSON.stringify(result, null, 4)));
I and a few others are seeing a similar issue with live data sources (in my case MongoDB/Mongoose). I suspect it is something internal to the graphql-tools makeExecutableSchema and the way it ingests text-based schemas with custom types.
Here's another post on the issue: How to use graphql-type-json package with GraphQl
I haven't tried the suggestion to build the schema in code, so can't confirm whether it works or not.
My current workaround is to stringify the JSON fields (in the connector) when serving them to the client (and parsing on the client side) and vice-versa. A little clunky but I'm not really using GraphQL to query and/or selectively extract the properties within the JSON object. This wouldn't be optimal for large JSON objects I suspect.
If anyone else comes here from Google results, the solution for me was to add the JSON resolver as parameter to the makeExecutableSchema call. It's described here:
https://github.com/apollographql/apollo-test-utils/issues/28#issuecomment-377794825
That made the mocking work for me.