I've made simple CRUD app with React and Apollo client on NestJS server with GraphQL API.
I have this simple Mutations:
schema.gql:
type Mutation {
createUser(input: CreateUserInput!): User! // CreateUserInput type you can see in user.input.ts below
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): User!
}
user.input.ts:
import { InputType, Field } from "#nestjs/graphql";
import { EmailScalar } from "../email.scalar-type";
#InputType()
export class CreateUserInput {
// EmailScalar is a custom Scalar GraphQL Type that i took from the internet and it worked well
#Field(() => EmailScalar)
readonly email: string;
#Field()
readonly name: string;
}
"EmailScalar" type checks if "email" input has *#*.* format basically
And when i make createUser Query to GraphQL API like this:
It cannot pass validation
(because Email type works fine)
But when Query sent from client - it passes validation:
NestJS server log (from code below)
users.resolver.ts:
#Mutation(() => User)
async createUser(#Args('input') input: CreateUserInput) { // Type from user.input.ts
Logger.log(input); // log from screenshot, so if it's here it passed validation
return this.usersService.create(input); // usersService makes requests to MongoDB
}
And it gets into MongoDB
Here is client side part:
App.tsx:
...
// CreateUserInput class is not imported to App.tsx (it is at server part) but it seems to be fine with it
const ADD_USER = gql`
mutation AddMutation($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`
function App(props: any) {
const { loading, error, data } = useQuery(GET_USERS);
const [addUser] = useMutation(
ADD_USER,
{
update: (cache: any, { data: { createUser } }: any) => {
const { users } = cache.readQuery({ query: GET_USERS });
cache.writeQuery({
query: GET_USERS,
data: {
users: [createUser, ...users],
},
})
}
}
);
...
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return <UserTable users={data.users} addUser={addUser} updateUser={updateUser} deleteUser={deleteUser} />;
}
Can someone please explain to me, how does client Query passes validation and what have i done wrong?
Even two empty strings can pass through.
Never worked with NestJS, Apollo, React or GraphQL before, so I'm kinda lost.
For full code:
https://github.com/N238635/nest-react-crud-test
This is how your custom scalar's methods are defined:
parseValue(value: string): string {
return value;
}
serialize(value: string): string {
return value;
}
parseLiteral(ast: ValueNode): string {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError('Query error: Can only parse strings got a: ' + ast.kind, [ast]);
}
// Regex taken from: http://stackoverflow.com/a/46181/761555
var re = /^([\w-]+(?:\.[\w-]+)*)#((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
if (!re.test(ast.value)) {
throw new GraphQLError('Query error: Not a valid Email', [ast]);
}
return ast.value;
}
parseLiteral is called when parsing literal values inside the query (i.e. literal strings wrapped in double quotes). parseValue is called when parsing variable values. When your client sends the query, it sends the value as a variable, not as a literal value. So parseValue is used instead of parseLiteral. But your parseValue does not do any kind of validation -- you just return the value as-is. You need to implement the validation logic in both methods.
It would also be a good idea to implement the serialize method so that your scalar can be used for both input and response validation.
Related
I define a schema like this:
const query = new GraphQLObjectType({
name: 'Query',
fields: {
quote: {
type: queryType,
args: {
id: { type: QueryID }
},
},
},
});
const schema = new GraphQLSchema({
query,
});
The QueryID is a customised scalar type.
const QueryID = new GraphQLScalarType({
name: 'QueryID',
description: 'query id field',
serialize(dt) {
// value sent to the client
return dt;
},
parseLiteral(ast) {
if (ast.kind === 'IntValue') {
return Number(ast.value);
}
return null;
},
parseValue(v) {
// value from the client
return v;
},
});
client query
query {
quote(queryType: 1)
}
I found that the parseValue method is not called when clients send query to my server. I can see parseLiteral is called correctly.
In most of the document I can find, they use gql to define schema and they need to put scalar QueryID in their schema definition. But in my case, I am using GraphQLSchema object for schema. Is this the root cause of that? If yes, what is the best way to make it works? I don't want to switch to gql format because I need to construct my schema at runtime.
serialize is only called when sending the scalar back to the client in the response. The value it receives as a parameter is the value returned in the resolver (or if the resolver returned a Promise, the value the Promise resolved to).
parseLiteral is only called when parsing a literal value in a query. Literal values include strings ("foo"), numbers (42), booleans (true) and null. The value the method receives as a parameter is the AST representation of this literal value.
parseValue is only called when parsing a variable value in a query. In this case, the method receives as a parameter the relevant JSON value from the variables object submitted along with the query.
So, assuming a schema like this:
type Query {
someField(someArg: CustomScalar): String
someOtherField: CustomScalar
}
serialize:
query {
someOtherField: CustomScalar
}
parseLiteral:
query {
someField(someArg: "something")
}
parseValue:
query ($myVariable: CustomScalar) {
someField(someArg: $myVariable)
}
I'm working on API based on NodeJs, GraphQl and using Typescript.
My app using eslint to mange style issues and I cannot understand how to rid off of the next one:
Missing return type on function.eslint(#typescript-eslint/explicit-function-return-type)
That message is shown in my VSC editor in the following code:
import { GraphQLScalarType, GraphQLError } from 'graphql';
import { Kind } from 'graphql/language';
export default new GraphQLScalarType({
name: 'Date',
description: 'Date type',
parseValue(value) {
// value comes from the client
return new Date(value); // sent to resolvers
},
serialize(value): Promise<string> {
// value comes from resolvers
return value.toISOString(); // sent to the client
},
parseLiteral(ast) {
// ast comes from parsing the query
// this is where you can validate and transform
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(`Query error: Can only parse dates strings, got a: ${ast.kind}`, [ast]);
}
if (isNaN(Date.parse(ast.value))) {
throw new GraphQLError(`Query error: not a valid date`, [ast]);
}
return new Date(ast.value);
},
});
The above code is a custom scalar for the date for the GraphQL to have the date in the right way.
I was able to rid off the message error on this line:
serialize(value): Promise<string> {...}
I just added Promise<string>
I would like to get help about what should be the right approach on the above code.
Presumably the problem is that you are missing a return type for parseValue() and parseLiteral(). So you should be able to just do:
parseValue(value): Date {
...
parseLiteral(ast): Date {
I'm really new to Graphql (just yesterday actually). I am "playing" around and try the various tools of the ecosystem (apollo-server, graphql.js ...ect).
For the sake of experimenting, I am trying to call a query from within nodejs (and not from a client in the browser, such as a react application)
First of all this is my simple schema along with resolvers:
export const mySchema = gql`
type User {
id: ID!
name:
surname: String
}
# root query has been defined in another file
extend type Query {
users: [User]
test: [User]
}
`
export const myResolvers = {
users: () => [ __array_of_users__ ],
test: () => /* this is where I would like to re-invoke the 'users query'
}
Using the makeExecutableSchema function, I create a schema object with my types and my resolvers and I export this schema into the apollo server application. Every thing works fine so far.
Now following this stackoverflow suggested solution, I created a helper function which should allow me to invoke a query defined in my schema as following:
import { graphql } from "graphql";
import { schema } from "./my-schema";
export const execute = str => {
return graphql(schema, str );
};
With this helper function, my resolvers become:
import { gql } from "apollo-server-express";
import { execute } from '__path_to_helper_function__';
export const myResolvers = {
users: () => [ __array_of_users__ ],
test: () => execute( gql`
query users {
name
}
`)
}
But in the playground, when I try the query:
{
test {
name
}
}
I get the following error:
I don't even know if what I am trying to do (to call a query from within node) can be done. Any suggestion will be greatly appreciated.
Thnaks
graphql-tag takes a string and parses it into a DocumentNode object. This is effectively the same as passing a String to the parse function. Some functions exported by the graphql module, like execute, expect to be passed in a DocumentNode object -- the graphql function does not. It should be passed just a plain String as the request, as you can see from the signature:
graphql(
schema: GraphQLSchema,
requestString: string,
rootValue?: ?any,
contextValue?: ?any,
variableValues?: ?{[key: string]: any},
operationName?: ?string
): Promise<GraphQLResult>
So, just drop the gql tag. You can see an (incomplete) API reference here.
I defined the below query in my GraphQL schema
type Query {
getShowByName(showSchemaName: String!): String!
}
with the corresponding resolver function as shown below
const resolvers = ()=>({
Query:{
getShowByName: function(args){
console.log("out1"+args);
console.log("out3"+args.showSchemaName);
return "hardcoded return from getShowByName";
},
},
});
In the graphql playground, i provided the below inputs
{
getShowByName(showSchemaName:"input to getShowByName")
}
The graphql playground provides hardcoded return from getShowByName as output in the playground page but in the terminal i get args as undefined. Hence, i am unable to parse the input fed in from the graphql playground.
Please help me in understanding where i am going wrong and how i can rectify the issue.
The first argument in the resolver is an object that contains the result of parent resolver. In your case, the root level Query will receive undefined. Instead you should extract the args from the second argument.
const resolvers = ()=> ({
Query: {
getShowByName: function(_, args) {
console.log("out1" + args);
console.log("out3" + args.showSchemaName);
return "hardcoded return from getShowByName";
},
},
});
In an update to our GraphQL API only the models _id field is required hence the ! in the below SDL language code. Other fields such as name don't have to be included on an update but also cannot have null value. Currently, excluding the ! from the name field allows the end user to not have to pass a name in an update but it allows them to pass a null value for the name in, which cannot be allowed.
A null value lets us know that a field needs to be removed from the database.
Below is an example of a model where this would cause a problem - the Name custom scalar doesn't allow null values but GraphQL still allows them through:
type language {
_id: ObjectId
iso: Language_ISO
auto_translate: Boolean
name: Name
updated_at: Date_time
created_at: Date_time
}
input language_create {
iso: Language_ISO!
auto_translate: Boolean
name: Name!
}
input language_update {
_id: ObjectId!
iso: Language_ISO!
auto_translate: Boolean
name: Name
}
When a null value is passed in it bypasses our Scalars so we cannot throw a user input validation error if null isn't an allowed value.
I am aware that ! means non-nullable and that the lack of the ! means the field is nullable however it is frustrating that, as far as I can see, we cannot specify the exact values for a field if a field is not required / optional. This issue only occurs on updates.
Are there any ways to work around this issue through custom Scalars without having to start hardcoding logic into each update resolver which seems cumbersome?
EXAMPLE MUTATION THAT SHOULD FAIL
mutation tests_language_create( $input: language_update! ) { language_update( input: $input ) { name }}
Variables
input: {
_id: "1234",
name: null
}
UPDATE 9/11/18: for reference, I can't find a way around this as there are issues with using custom scalars, custom directives and validation rules. I've opened an issue on GitHub here: https://github.com/apollographql/apollo-server/issues/1942
What you're effectively looking for is custom validation logic. You can add any validation rules you want on top of the "default" set that is normally included when you build a schema. Here's a rough example of how to add a rule that checks for null values on specific types or scalars when they are used as arguments:
const { specifiedRules } = require('graphql/validation')
const { GraphQLError } = require('graphql/error')
const typesToValidate = ['Foo', 'Bar']
// This returns a "Visitor" whose properties get called for
// each node in the document that matches the property's name
function CustomInputFieldsNonNull(context) {
return {
Argument(node) {
const argDef = context.getArgument();
const checkType = typesToValidate.includes(argDef.astNode.type.name.value)
if (checkType && node.value.kind === 'NullValue') {
context.reportError(
new GraphQLError(
`Type ${argDef.astNode.type.name.value} cannot be null`,
node,
),
)
}
},
}
}
// We're going to override the validation rules, so we want to grab
// the existing set of rules and just add on to it
const validationRules = specifiedRules.concat(CustomInputFieldsNonNull)
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules,
})
EDIT: The above only works if you're not using variables, which isn't going to be very helpful in most cases. As a workaround, I was able to utilize a FIELD_DEFINITION directive to achieve the desired behavior. There's probably a number of ways you could approach this, but here's a basic example:
class NonNullInputDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
const { args: { paths } } = this
field.resolve = async function (...resolverArgs) {
const fieldArgs = resolverArgs[1]
for (const path of paths) {
if (_.get(fieldArgs, path) === null) {
throw new Error(`${path} cannot be null`)
}
}
return resolve.apply(this, resolverArgs)
}
}
}
Then in your schema:
directive #nonNullInput(paths: [String!]!) on FIELD_DEFINITION
input FooInput {
foo: String
bar: String
}
type Query {
foo (input: FooInput!): String #nonNullInput(paths: ["input.foo"])
}
Assuming that the "non null" input fields are the same each time the input is used in the schema, you could map each input's name to an array of field names that should be validated. So you could do something like this as well:
const nonNullFieldMap = {
FooInput: ['foo'],
}
class NonNullInputDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
const visitedTypeArgs = this.visitedType.args
field.resolve = async function (...resolverArgs) {
const fieldArgs = resolverArgs[1]
visitedTypeArgs.forEach(arg => {
const argType = arg.type.toString().replace("!", "")
const nonNullFields = nonNullFieldMap[argType]
nonNullFields.forEach(nonNullField => {
const path = `${arg.name}.${nonNullField}`
if (_.get(fieldArgs, path) === null) {
throw new Error(`${path} cannot be null`)
}
})
})
return resolve.apply(this, resolverArgs)
}
}
}
And then in your schema:
directive #nonNullInput on FIELD_DEFINITION
type Query {
foo (input: FooInput!): String #nonNullInput
}