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.
Related
I am trying to write a test for an angular service which I am using with graphql & Apollo.
I'm receiving these errors: Error: Expected one matching operation for criteria "Match DocumentNode", found none.
and
Error: Expected no open operations, found 1:
foo.spec.ts
import { TestBed } from '#angular/core/testing';
import {
....
const fragment: DocumentNode = gql`
fragment SummaryDetails on SummaryDetails {
foo
...
}
`;
const summaryQuery: DocumentNode = gql`
query Summary(
) {
Summary(
) {
...SummaryDetails
}
}
${fragment}
`;
const fundDataServiceParams: FundDataServiceParams = {
foo ...
};
const expectedRawSummaryDetails: any = {
Summary: [
{
foo ...
},
],
};
const expectedSummaryDetails: SummaryDetail[] = [
{
foo ...
},
];
describe('FundSummaryServiceRegisterMethod', () => {
let controller: ApolloTestingController;
let fundSummaryService: FundSummaryService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApolloTestingModule, HttpClientTestingModule],
});
controller = TestBed.inject(ApolloTestingController);
foo = TestBed.inject(foo);
});
afterEach(() => {
controller.verify();
});
fit('should return foodetails', (done: DoneFn) => {
fundSummaryService
.register(fragment, fundDataServiceParams)
.subscribe((foo) => {
expect(foo).toEqual(foo);
done();
});
const op = controller.expectOne(fooQuery);
expect(op.operation.variables.foo).toEqual(foo.foo);
op.flushData(expectedfoo);
});
});
Note: I tried this old post's suggestion, didn't work.
The good news for you is that in the afterEach, controller.verify() is failing meaning there is a query in queue and not all of them have been dealt with. I am thinking that your summaryQuery is slightly off where it is not matching exactly the query where the afterEach is saying is still in queue and has not been flushed.
I think you can use a different definition of expectOne from the documentation to match the query in queue. I don't have much experience with Apollo or GraphQL but try something like this for the expectOne:
const op = controller.expectOne(operation => {
// can assert some stuff on the operation like the documentation shows
return true;
});
If the above works, it most likely means that summaryQuery does not match the query that ApolloTestingController sees.
I have aproblem when test Apollo.When I try query with apollo and graphql, i want response return error and partical data, so I set property errorPolicy:'all'. But its not work. I don't no why? Help please!
Here my code:
query { animal {
name
age }, school {
name
numberfd } } `
const { loading,data,error} = useQuery(GET_DASHBOARD_DATA, {
errorPolicy:'all',
onCompleted: (res) => {console.log("complete",res)},
onError : (res,data) => {console.log("ERRRR",res,data)},
})
and i want to receive:
{
error:[...], data:[animal:[...]] }
but its only response error.Here is Apollo's doc: https://www.apollographql.com/docs/react/data/error-handling/
onError type is onError?: (error: ApolloError) => void;. You don't have data inside onError callback.
After useQuery you can add:
console.log('data', data)
console.log('error', error)
I faced the same issue with errorPolicy: 'all', I only received the partial result inside onCompleted callback of useQuery, but no errors.
I created an ErrorLink like this:
private createErrorLink = () => {
return new ApolloLink((operation, forward) => {
return forward(operation).map((response) => {
// filter out errors you don't want to display
const errors = filterSomeErrors(response.errors);
if (errors && response?.data) {
response.data.errors = errors;
}
return response;
});
});
};
Now inside my onCompleted callback I get my data as well as errors. You will have to tweak your types a bit, because seems there is no errors field on response.data by default.
Mind that if you use onError from Apollo and return something from the link, it will retry your request containing errors!
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...
I am looking to write what I am calling structural expectations with Jest and I am not sure how this could be accomplished.
To start I have a graphql server and a database with a number of todo items. I currently have the following test that just returns true if the content within the database is the same as the response that I have written. I want to check instead that the response looks like an object with data that could be anything.
Here is the code that I have:
describe('To Do:', () => {
it('add todo items', async () => {
const response = await axios.post('http://localhost:5000/graphql', {
query: `
query {
getTodoItems {
message
id
dateCreated
dateDue
}
}
`
});
const { data } = response;
expect(data).toMatchObject({
data: {
getTodoItems: [
{
message: "message",
id: "5bd9aec8406e0a2170e04494",
dateCreated: "1540992712052",
dateDue: "1111111111"
},
{
message: "message",
id: "5bd9aeec60a9b2579882a308",
dateCreated: "1540992748028",
dateDue: "1111111111"
},
{
message: "new message",
id: "5bd9af15922b27236c91837c",
dateCreated: "1540992789836",
dateDue: "1111111111"
}
]
}
})
});
});
Now I want to write something like this, where there can be any number of returned items and they follow similar structuring:
describe('To Do:', () => {
it('add todo items', async () => {
const response = await axios.post('http://localhost:5000/graphql', {
query: `
query {
getTodoItems {
message
id
dateCreated
dateDue
}
}
`
});
const { data } = response;
expect(data).toMatchObject({
data: {
getTodoItems: [
{
message: expect.any(String),
id: expect.any(String),
dateCreated: expect.any(String),
dateDue: expect.any(String)
} // There needs to be unlimited additional items here
]
}
})
});
});
I have been looking throught the docs and I even tried nesting the expectations but I can't seem to get the desired response. Let me know what yo think or if I can clarify in any way.
I figured out the best way for me to do it. I would love to hear better answers. I wrote a function within the scope of the test as a jest.fn and then I called it. In that function, I made custom checks to parse the data that was received in the response. From there I added an expect function with the 'toHaveReturnedWith' method to see what the response of my custom function was and finishing out the test.
const addTodoResponse = jest.fn(() => {
// Custom parsing and check here
// Returns true or false
});
addTodoResponse();
expect(addTodoResponse).toHaveReturnedWith(true);
Are there better ways to do this out there?
I've been trying to research on how to add another root property of a GraphQL response but found nothing after 1 hour.
Normally, a GraphQL query looks like this:
{
myQuery() {
name
}
}
It responds with:
{
"data": {
"myQuery": []
}
}
I'm curious if I can add another root property in this response say "meta"
{
"data": {
"myQuery": []
},
"meta": {
"page": 1,
"count": 10,
"totalItems": 90
}
}
Is this possible, if not what's the best approach in tackling this with respect to GraphQL?
Thanks!
The apollo-server middleware can be configured with a number of configuration options, including a formatResponse function that allows you to modify the outgoing GraphQL response
const formatResponse = (response) => {
return {
meta
...response
}
}
app.use('/graphql', bodyParser.json(), graphqlExpress({
schema,
formatResponse,
}));
You could pass the req object down to your context, mutate it within your resolver(s) and then use the result inside formatResponse. Something like...
app.use('/graphql', bodyParser.json(), (req, res, next) => graphqlExpress({
schema,
formatResponse: (gqlResponse) => ({
...gqlResponse
meta: req.metadata
}),
})(req, res, next));
Typically, though, you would want to include the metadata as part of your actual schema and have it included with the data. That will also allow you to potentially request multiple queries and get the metadata for all of them.
There's any number of ways to do that, depending on how your data is structured, but here's an example:
type Query {
getFoos: QueryResponse
getBars: QueryResponse
}
type QueryResponse {
results: [Result]
meta: MetaData
}
union Result = Bar | Foo
You can add anything in the response as well... Please follow below code.
app.use('/graphql', bodyParser.json(), graphqlExpress(req => {
return {
schema: tpSchemaNew,
context: {
dbModel
},
formatError: err => {
if (err.originalError && err.originalError.error_message) {
err.message = err.originalError.error_message;
}
return err;
},
formatResponse : res => {
res['meta'] = 'Hey';
return res;
}
}
}))
Apollo Server-specific:
Just adding to the previous answers that formatResponse() has another useful argument, requestContext.
If you are interested in extracting values from that (for example, the context passed to the resolver), you can do the following. BEWARE HOWEVER, the context will likely contain sensitive data that is supposed to be private. You may be leaking authentication data and secrets if not careful.
const server = new ApolloServer({
schema,
formatResponse: (response, requestContext) => {
//return response
const userId = requestContext.context.user.id
response = Object.assign(response, {
extensions: {
meta: {
userId: userId
}
}
}
return response
},
})
The above will return something like this in the gql query response (note the extensions object):
{
data: {
user: {
firstName: 'Hello',
lastName: 'World'
}
},
extensions: { // <= in Typescript, there is no `meta` in GraphQLResponse, but you can use extensions
meta: {
userId: 1234 //<= data from the context
}
}
}
The full list of properties available in requestContext:
at node_modules/apollo-server-types/src/index.ts>GraphQLRequestContext
export interface GraphQLRequestContext<TContext = Record<string, any>> {
readonly request: GraphQLRequest;
readonly response?: GraphQLResponse;
readonly context: TContext;
readonly cache: KeyValueCache;
// This will be replaced with the `operationID`.
readonly queryHash?: string;
readonly document?: DocumentNode;
readonly source?: string;
// `operationName` is set based on the operation AST, so it is defined even if
// no `request.operationName` was passed in. It will be set to `null` for an
// anonymous operation, or if `requestName.operationName` was passed in but
// doesn't resolve to an operation in the document.
readonly operationName?: string | null;
readonly operation?: OperationDefinitionNode;
/**
* Unformatted errors which have occurred during the request. Note that these
* are present earlier in the request pipeline and differ from **formatted**
* errors which are the result of running the user-configurable `formatError`
* transformation function over specific errors.
*/
readonly errors?: ReadonlyArray<GraphQLError>;
readonly metrics?: GraphQLRequestMetrics;
debug?: boolean;
}