graphql icon indicating copy to clipboard operation
graphql copied to clipboard

Input Validation

Open vinerich opened this issue 3 years ago • 3 comments

Is your feature request related to a problem? Please describe. I'm always frustrated when trying to validate inputs. Not the best description but it surely hits the point.

These are actually 2 issues that I'll try to outline below. The exact use cases and approaches are still under discussion so feel free to criticize them.

Validation

Having the whole graph structure together in one(!) file already helps a ton! It came naturally to our project to integrate validation logic with joi (e.g.) into our type definitions.

The thought was to not tamper with different files for this and that but to define everything in one go and see on a quick glance what a property is about.

Typescript type generation

This is not the ogm repository but a related issue that will have the need to be opened there. Another use case of having the validation definition in the same schema, is the ability to generate it in the ogm files.

This would give the user the ability to call the same(!) validation manually if needed. E.g.: monorepo + javascript frontend. There would be no need to separately track two different validation options.

Describe the solution you'd like I'd like to have the ability to write validation logic with a simple fluent interface like https://github.com/sideway/joi. This could be applied through a directive which takes additional arguments for input and query validation.

Describe alternatives you've considered This is the solution we currently have implemented. We defined a directive like so:

directive @validate(
        joi: String!
      ) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION

Which can be used like this:

email: String @validate(joi: ${Joi.string().email().describe().gql()})

The .gql() function is a double JSON.stringify() call to prevent parsing issues from the used gql-parser. The resulting string can be parsed back with Joi.build(JSON.parse(args.joi as string)) as Schema.

The neo4j-graphql library does not apply these directives to the generated INPUT_OBJECT_FIELD_DEFINITIONs, but only to the OBJECT_FIELD_DEFINITION which correspond to the query types.

To apply our directive to input_types (which input validation is all about) we applied the following schema-mapping:

    const validateDirectiveTransformer = (schema: GraphQLSchema) =>
      mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
          const directive = getDirective(
            schema,
            fieldConfig,
            directiveName
          )?.[0];

          if (directive) {
            wrapType(fieldConfig, directive);
            directiveMap.add(typeName, fieldName, {
              directive: fieldConfig.astNode.directives[0],
              directiveArgumentMap: directive,
            });
            return fieldConfig;
          }
        },
        [MapperKind.INPUT_OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
          const node = directiveMap.get(typeName, fieldName);
          if (node) {
            wrapType(fieldConfig, node.directiveArgumentMap);
            return fieldConfig;
          }
        },
      }),

In essence we take the validate directive from the query type, save a unique identifier consisting of fieldname and typename and later apply that to the input type if we encounter the same identifier.

Additional context Add any other context or screenshots about the feature request here.

vinerich avatar May 10 '22 08:05 vinerich

I'm also waiting for such a feature. In our project this is one of the biggest topics of discussion. Recently I came up with the following idea/workaround based on the callback directive. I haven't fully tested this yet though. Unfortunately, this would currently only work for primitive types, not for relationships.

type Product {
    productTitle: String
    evenNumberSetter: Int @writeonly
    evenNumber: Int @callback(operations: [CREATE, UPDATE], name: "validateEvenNumber")
}
const validateEvenNumber = async (parent, args, context) => {
    if (!parent.evenNumberSetter) {
        return undefined;
    }
    if (parent.evenNumberSetter % 2 != 0) {
        throw new ApolloError("Validation Error");
    }
    return parent.evenNumberSetter;
}

...

const neoSchema = new Neo4jGraphQL({ 
    typeDefs, 
    driver,
    config: {
        callbacks: {
          validateEvenNumber: validateEvenNumber,
        },
    },
});

I found that the callback directive does nothing on the return value "undefined". The existing property of the node is therefore not reset either. I'm not sure if this is really a feature of the callback directive, though. This means that an update also works if you don't use the "setter" field.

AccsoSG avatar Jul 11 '22 21:07 AccsoSG

The easiest way to apply validations at the moment is either to use an existing directive like graphql-constraint-directive or graphql-middleware and specifically graphql-yup-middleware

litewarp avatar Jul 11 '22 22:07 litewarp

@litewarp thank you! Are there any examples of using this Tools with neo4j graphql?

AccsoSG avatar Jul 11 '22 23:07 AccsoSG

There's some good ideas here for an eventual solution, but going to close this in favour of #138 which has been around for (a lot) longer and really the principal is the same. Thanks!

darrellwarde avatar Aug 25 '22 16:08 darrellwarde