I am using Apollo Server to build a graphql based server that interfaces with a MongoDB instance. I want to make it so that if the client passes a value for a field that has a graphql type of ID, the value must be a valid mongo ObjectId (mongoose has a util to check this I think -- that's not the issue). That is, if the value isn't a valid ObjectId, I would expect to get a validation error at the graphql level (i.e. a 400). I don't want to get a 500 later in my server when I try to use that value as an ObjectId, and I don't want to have to check that the value is a valid ObjectId at multiple places within the server.
Basically I want to implement validation logic at the graphql level for the ID scalar type, if that makes sense.
You'd need to utilize a custom scalar instead of ID to get the sort of behavior you're looking for. You can write your own or use an existing one.
import { Kind, GraphQLError, GraphQLScalarType, ValueNode } from 'graphql';
const MONGODB_OBJECTID_REGEX = /*#__PURE__*/ new RegExp(/^[A-Fa-f0-9]{24}$/);
export const GraphQLObjectID = /*#__PURE__*/ new GraphQLScalarType({
name: 'ObjectID',
description:
'A field whose value conforms with the standard mongodb object ID as described here: https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectId. Example: 5e5677d71bdc2ae76344968c',
serialize(value: string) {
if (!MONGODB_OBJECTID_REGEX.test(value)) {
throw new TypeError(
`Value is not a valid mongodb object id of form: ${value}`,
);
}
return value;
},
parseValue(value: string) {
if (!MONGODB_OBJECTID_REGEX.test(value)) {
throw new TypeError(
`Value is not a valid mongodb object id of form: ${value}`,
);
}
return value;
},
parseLiteral(ast: ValueNode) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as mongodb object id but got a: ${ast.kind}`,
);
}
if (!MONGODB_OBJECTID_REGEX.test(ast.value)) {
throw new TypeError(
`Value is not a valid mongodb object id of form: ${ast.value}`,
);
}
return ast.value;
},
});
I'm implementing GraphQL to query data from the web with web driver.
My problem is that I cannot figure out how to use one argument as input for 2 different queries. So basically It is querying data from 2 different sites and has the same input which is usually 4 character Symbol.
What I want my query to look like.
{
Webpage1(symbol:"AABC"){
data_from_site,
some_other_data
Webpage2(symbol:"AABC"){ ##get rid of this Double attribute entry
data_from_page2
}
}
}
How can I pass only one argument be And get data from both sites?
So it will be in the root context for the resolver to use.
I'm fairly new to GraphQL and have tried defining separate GraphQLObjectType in order to solve this problem. But what I'm really looking for is unified data in the same object and arguments parsed from the parent object.
var schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
Webpage1: {
type: Website1,
args: {
symbol: { type: GraphQLString },
//date: { type: GraphQLString }
},
resolve: function (_, { symbol }) {
return {
symbol
};
}
},
Webpage2: {
type: History,
resolve: function (_, { symbol }) {
return {
symbol
};
}
}
}
})
})
If I'm understanding your question correctly, you can keep your schema as is and just utilize a variable when sending your query:
query MyQuery($mySymbol: String){
Webpage1(symbol:$mySymbol) {
data_from_site
some_other_data
}
Webpage2(symbol:$mySymbol) {
data_from_page2
}
}
Variables are defined at the top of your document, as part of your operation definition. Once defined, they can used any number of times inside the document anywhere you would use the same kind of literal value. You can check out the official tutorial on variables here and the spec here.
Getting this error from Apollo:
core.js:14576 ERROR Error: Network error: Error writing result to store for query:
{"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdditionalServices"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"vendorID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},"directives":[]}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vendor"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"vendorID"}}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"services"},"name":{"kind":"Name","value":"products"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"productTypes"},"value":{"kind":"EnumValue","value":"service"}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"isActive"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"cartSection"},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"name"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"description"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"name"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}],"loc":{"start":0,"end":372}}
Store error: the application attempted to write an object with no provided id but the store already contains an id of Restaurant:200 for this object. The selectionSet that was trying to be written is:
{"kind":"Field","name":{"kind":"Name","value":"vendor"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"vendorID"}}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"services"},"name":{"kind":"Name","value":"products"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"productTypes"},"value":{"kind":"EnumValue","value":"service"}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"isActive"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"cartSection"},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"name"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"description"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"shortDescription"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"name"},"arguments":[],"directives":[]},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}
at new ApolloError (ApolloError.js:25)
at QueryManager.js:276
at QueryManager.js:638
at Array.forEach (<anonymous>)
at QueryManager.js:637
at Map.forEach (<anonymous>)
at QueryManager.push../node_modules/apollo-client/core/QueryManager.js.QueryManager.broadcastQueries (QueryManager.js:632)
at QueryManager.js:226
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
at Object.onInvoke (core.js:16135)
This is the code that makes this happen:
this.restaurantID$.pipe(
takeUntil(this._ngOnDestroy)
)
.subscribe((restaurantID) => {
this.additionalServicesQuery$.next(this._apollo
.watchQuery<AdditionalServices>({
query: AdditionalServicesQuery,
variables: { vendorID: restaurantID }
}));
});
const loadAdditionalServicesData = this.additionalServicesQuery$
.pipe(
takeUntil(this._ngOnDestroy),
filter((query) => !!query),
switchMap((query) => query.valueChanges), // This is the switchMap that makes it happen
takeUntil(this._ngOnDestroy),
map((response) => response.data.vendor.services.nodes)
);
There is a SwitchMap that I commented if that is removed, the error does not happen. I can't understand what is going on.
query:
export const AdditionalServicesQuery = gql`
query AdditionalServices(
$vendorID: ID!
) {
vendor(
id: $vendorID
) {
services: products (productTypes: service) {
nodes {
id
isActive
cartSection {
id
name
}
description
imageUrl
shortDescription
name
}
}
}
}
`;
Update:
Added ID to query, still, same issue
export const AdditionalServicesQuery = gql`
query AdditionalServices(
$vendorID: ID!
) {
vendor(
id: $vendorID
) {
services: products (productTypes: service) {
id
nodes {
id
isActive
cartSection {
id
name
}
description
imageUrl
shortDescription
name
}
}
}
}
`;
According to that error, you need to add an id field (or _id field, whichever exists) to the selection set for the vendor field. Sounds like you already have another query that returns objects of the type Restaurant, that query included the id and was normalized properly. Apollo won't be able to combine the individual Restaurant objects from both queries unless an id is included.
export const AdditionalServicesQuery = gql`
query AdditionalServices(
$vendorID: ID!
) {
vendor(
id: $vendorID
) {
id # <----------------------------------------- ADD ME
services: products (productTypes: service) {
nodes {
id
isActive
cartSection {
id
name
}
description
imageUrl
shortDescription
name
}
}
}
}
`;
I have the following the code snippet:
export const NodeInterface = new GraphQLInterfaceType({
name: 'Node',
fields: {
id: {
type: new GraphQLNonNull(GraphQLID)
}
},
resolveType: (source) => {
if (source.__tableName === tables.users.getName()) {
return UserType;
}
return PostType;
}
});
and a GraphQLObjectType that is using the interface:
export const PostType = new GraphQLObjectType({
name: 'Post',
interfaces: [ NodeInterface ],
fields: {
id: {
type: new GraphQLNonNull(GraphQLID),
resolve: resolveId
},
createdAt: {
type: new GraphQLNonNull(GraphQLString),
},
body: {
type: new GraphQLNonNull(GraphQLString)
}
}
});
For what do I have to define an interface?
In GraphQL, interfaces fulfill two purposes:
They ensure that the types implementing them also implement specific fields. For example, the Node interface here has an id field -- that means any type that implements the Node interface will also need to have an id field (and that id will need to be an ID scalar like in the interface) The same goes for arguments on those -- any arguments on the fields in the interface will also have to exist on the matching fields in the implementing type.
They can be used when two or more types are expected for a field. A field will always resolve to exactly one type or scalar, however, by using interfaces (or unions) we indicate in our schema that the field could resolve to one of a set of types.
So let's say we have a Node like in your snippet, some types that implement it, and a query that returns a Node:
interface Node {
id: ID!
}
type Foo implements Node {
id: ID!
someFooField: String!
someOtherFooField: Int!
}
type Bar implements Node {
id: ID!
someBarField: String!
someOtherFooField: Int!
}
type Query {
getNode(id: ID!): Node!
}
In our example, getNode could resolve to either a Foo or a Bar. When we write our query, we don't know which one will be resolved. But because we know the id field is required by the interface, we can write a query like this:
query OperationName {
getNode(id: "SOME_ID"){
id
}
}
If we need to query someBarField as well, though, we can't do this:
query OperationName {
getNode(id: "SOME_ID"){
id
someBarField
}
}
because Foo doesn't have that field. Instead we have to utilize a fragment, like this:
query OperationName {
getNode(id: "SOME_ID"){
id
... on Bar {
someBarField
}
}
}
Then someBarField will be returned, but only if the field resolves to the type Bar. If it's a Foo, only id will be returned. Similarly, you can request non-shared fields from any type implementing the same interface:
query OperationName {
getNode(id: "SOME_ID"){
id
... on Bar {
someBarField
}
... on Foo {
someFooField
}
}
}
Last but not least, it should be mentioned that unions work in a very similar fashion. However, unlike interfaces, there are no shared fields defined for a Union, so a type does not "implement" a union, it just is part of one. That means when requesting a field that returns a union, you'll always have to use fragments since there are no shared fields to request.
See graphql mutation with JavaScript variables injected (please ignore that it's constructed manually and not through some gql helper):
mutation {
createPost(
name: "${name}",
description: "${description}"
) { id }
}
Is there any standard function for escaping name & description string variables ?
Eg. some function gqlEscape which will make query safe:
mutation {
createPost(
name: "${gqlEscape(name)}",
description: "${gqlEscape(description)}"
) { id }
}
How would that function look like?
Instead of doing string interpolation, you can use GraphQL variables which are designed for your use case. You can do this using raw HTTP, too, but I usually use graphql-request in this case.
Here's an example:
import { request } from 'graphql-request'
const mutation = `
mutation newPost($name: String!, $description: String) {
createPost(
name: $name,
description: $description
) {
id
}
}
`
const variables = {
name: 'New Post',
}
request('my-endpoint', mutation, variables).then(data => console.log(data))
A few notes:
I defined two variables, name: String! and description: String. String! means, it's a required variable of type String, while String means it's an optional variable. You can see that I did not pass a value for description, which is possible because it's optional.
I did not specify the double quotes for the two strings, that's already handled by the GraphQL Variables themselves.
You can read more about GraphQL Variables here and here.