How do should I approach a local server in conjunction with graphQL schemas? - graphql

(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).

Related

Why do I not have an Apollo cache-hit between a multi-response query and a single-item query for the same type?

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! :)

How to get all cache data using reactjs #apollo/client v3

Is there anyway that I can check all the cache, eg: changes within apollo for debugging.
Something like redux store, where you can view the whole state tree.
They mentioned:
The cache stores the objects by ID in a flat lookup table.
https://www.apollographql.com/docs/react/caching/cache-configuration/
Any way to display/console the whole lookup table?
For #apollo/client v3
Found the answer, if anyone is interested.
Through InMemoryCache
You can console log the cache object where you create with InMemoryCache.
You should be able to find it under your created cache:
const cache = new InMemoryCache({"...Your option"})
console.log(cache.data) // <- Your cache query
Through browser console
Through browser, use console to log data
__APOLLO_CLIENT__.cache.data
Through apollo v3
Access through apollo client cache
const client = useApolloClient();
const serializedState = client.cache.extract();
console.log(serializedState) <- your cache query
I just installed Apollo Client extension for Chrome, seem to work, there is now "Apollo" tab in the dev tools
https://chrome.google.com/webstore/detail/apollo-client-devtools/jdkknkkbebbapilgoeccciglkfbmbnfm?hl=en-US
I used "readQuery" method to get data from cache, might be not best way to do it.
import { useApolloClient, gql } from '#apollo/client';
...
const WEBSITE_TITLE = gql`
query GetSitewide {
sitewide {
data {
attributes {
header {
__typename
id
siteTitle
}
}
}
}
}
`;
...
function WebsiteTitle() {
const client = useApolloClient();
const {
sitewide: {
data: {
attributes: { header },
},
},
} = client.readQuery({
query: WEBSITE_TITLE,
});
const { siteTitle } = header;
return <> {siteTitle} </>;
}
export default WebsiteTitle;

How do you make Schema Stitching in Apollo Server faster?

Initially, I tried to use a Serverless Lambda function to handle schema stitching for my APIs, but I started to move toward an Elastic Beanstalk server to keep from needing to fetch the initial schema on each request.
Even so, the request to my main API server is taking probably ten times as long to get the result from one of the child API servers as my child servers do. I'm not sure what is making the request so long, but it seems like there is something blocking the request from resolving quickly.
This is my code for the parent API:
import * as express from 'express';
import { introspectSchema, makeRemoteExecutableSchema, mergeSchemas } from 'graphql-tools';
import { ApolloServer } from 'apollo-server-express';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
async function run () {
const createRemoteSchema = async (uri: string) => {
const link = new HttpLink({ uri, fetch });
const schema = await introspectSchema(link);
return makeRemoteExecutableSchema({
schema,
link
});
};
const remoteSchema = await createRemoteSchema(process.env.REMOTE_URL);
const schema = mergeSchemas({
schemas: [remoteSchema]
});
const app = express();
const server = new ApolloServer({
schema,
tracing: true,
cacheControl: true,
engine: false
});
server.applyMiddleware({ app });
app.listen({ port: 3006 });
};
run();
Any idea why it is so slow?
UPDATE:
For anyone trying to stitch together schemas on a local environment, I got a significant speed boost by fetching 127.0.0.1 directly instead of going through localhost.
http://localhost:3002/graphql > http://127.0.0.1:3002/graphql
This turned out not to be an Apollo issue at all for me.
I'd recommend using Apollo engine to observe what is really going on with each request as you can see on the next screenshot:
you can add it to your Apollo Server configuration
engine: {
apiKey: "service:xxxxxx-xxxx:XXXXXXXXXXX"
},
Also, I've experienced better performance when defining the defaultMaxAge on the cache controle:
cacheControl: {
defaultMaxAge: 300, // 5 min
calculateHttpHeaders: true,
stripFormattedExtensions: false
},
the other thing that can help is to add longer max cache age on stitched objects if it does make sense, you can do this by adding cache hints in the schema stitching resolver:
mergeSchemas({
schemas: [avatarSchema, mediaSchema, linkSchemaDefs],
resolvers: [
{
AvatarFlatFields: {
faceImage: {
fragment: 'fragment AvatarFlatFieldsFragment on AvatarFlatFields { faceImageId }',
resolve(parent, args, context, info) {
info.cacheControl.setCacheHint({maxAge: 3600});
return info.mergeInfo.delegateToSchema({
schema: mediaSchema,
operation: 'query',
fieldName: 'getMedia',
args: {
mediaId: parseInt(parent.faceImageId),
},
context,
info,
});
}
},
}
},
Finally, Using dataLoaders can make process requests much faster when enabling batch processing and dataloaders caching read more at their github and the code will be something like this:
public avatarLoader = (context): DataLoader<any, any> => {
return new DataLoader(ids => this.getUsersAvatars(dataLoadersContext(context), ids)
.then(results => new Validation().validateDataLoaderArrayResults(ids, results))
, {batch: true, cache: true});
};

fieldASTs on GraphQL resolve

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.

mongoose populate different database

I have two databases in my program. One for saving users and roles and one for the rest.
1) my model User has a reference to Roles. So I can populate roles by doing:
User.find({}).populate('local.roles').sort({ email: 1 }).exec(function (err, users) { ...
This works perfect and I can get a user and his roles.
2) When I try to do the same, but with models that have a connection to another database, I get the infamous error:
"MissingSchemaError: schema hasn't been registered for model ..."
This is the way I code my models when they use a different connection:
var mongoose = require('mongoose');
// to define another mongoose connection
var configScrwebDB = require('./../../config/scrwebDatabase.js');
var scrwebDBConnection = mongoose.createConnection(configScrwebDB.url);
var aseguradoSchema = mongoose.Schema({
nombre: { type: String },
abreviatura: { type: String },
rif: { type: String },
direccion: { type: String }
});
module.exports = scrwebDBConnection.model('Asegurado', aseguradoSchema);
3) this is what I do to 'populate' some field in my query (and the one that fails with above error):
var query = Riesgo.find({ cia: filtroObject.ciaSeleccionada });
query.populate('asegurado');
query.sort("codigo");
query.select("codigo fechaInicio estado moneda");
query.exec(function (err, riesgos) { ...
Of course this is in another 'js' file; and I do my 'require', etc., in order to import my models; etc.
As I said before, I can populate when models use 'default' mongoose connection.
Any ideas how I should correct this will be appreciated ... Am I missing some obvious thing here?
Thanks and bye ...
I had the same error. You could use:
var query = Riesgo.find({ cia: filtroObject.ciaSeleccionada });
query.populate('asegurado','fields to retrieve',User);// User need to be defined in the file as an variable that contents the User model defined.
query.sort("codigo");
query.select("codigo fechaInicio estado moneda");
Refer link

Resources