graphql-perl icon indicating copy to clipboard operation
graphql-perl copied to clipboard

Allow one to associate Directives to Types via the Perl API

Open jjn1056 opened this issue 6 years ago • 6 comments

The GraphQL::Directive class makes it easy to programmatically create new Directives and add them to your Schema. I would like to make it equally easy to associate these Directives with GraphQL::Type classes that you might use to programmatically create the various types. Although you can also do this with the GraphQL Schema Language I believe it will be very useful to also be able to add them via the Perl API (by this I mean the GraphQL::Type API for example). There are legitimate use cases where one wishes to generate a Schema programmatically (for example by introspecting a DBIx::Class representation of a database). Additionally one readily finds examples of this support in other languages, which is a good example of the fact some programmers find this ability useful. For example: https://www.npmjs.com/package/graphql-custom-directive

  • Please note this ticket is a proposal only to be able to add Directives to types via the Perl API as meta data. It is not a proposal on how we might have a server side API to handle those Directives. That will be a separate ticket.

Here's only possible approach for discussion

use GraphQL::Directive;
my $directive = GraphQL::Directive->new(
  name => 'IsAuthorized',
  description =>  'Is the user authorized to see this?',
  locations => ['FIELD'],
);

use GraphQL::Type::Object;
my $type = GraphQL::Type::Object->new(
  name => 'Object',  
  fields => {
    field_name => { 
      type => $scalar_type,
      directives => [
        {
             name => 'isAuthorized',
         },
      ],
    },
  },
);

Basically we'd probably have a new Role for storing directives, associate that with all the relevant classes and then add the code in the places we build the AST or Doc.

jjn1056 avatar Jun 01 '19 14:06 jjn1056

As discussed, please give actual queries your FE folks expect to run.

mohawk2 avatar Jun 12 '19 14:06 mohawk2

Here's a use case. My front end team does some validations on forms that are a mix of fully client side validation (like number of characters) and 'queries to a back end server' validation. For example when display a form for a client to create an account we ask for their name and form them to choose a unique username. For three fields (first_name, last_name and user_name). Client side we validate that those fields have the proper length mins and maxes and check for disallowed characters etc. That validation happens dynamically (via javascript when a client exists the field, before hitting the 'submit button'. That way we give them immediate feedback (and we avoid a POST to our already overloaded server).

This also happens for many server side validation requirements. So when a client types in a user name and completes the field, the FE team issues a GET to the 'all user_names cache DB' (which is a stand alone Redis API that syncs with the production DB) to make sure the chosen name is actually unique. This happened before the client clicks submit and sends all the form data to the main application server. This way the client gets immediate error feedback on each field as its completed rather than waiting for a big list of errors after clicking 'submit'. This also lowers load on on production server. BTW this query could in theory be a POST to a GraphQL query if we do this right :)

My FE team is requesting that the GraphQL schema we generate contains Directive tags for things like 'this is a unique field' that way they can generate the correct validations without having to hardcode that information. They can just write code that reads the type info and gets from the directives things like 'Unique' and other things like Max and Min lengths, etc. Then they can avoid hardcoding in the client side and we can be more agile and make changes server side that they don't need to rewrite code for. For example they would like a type info that looks like this:

type User {
  first: String! @length(max: 50)
  last: String! @length(max: 50)
  username: String! @length(max: 50, min: 3) @unique
}

So then they would have javascript that inspects that and builds client side validation code that automatically enforces the rules, instead of hard coding all those validation on a per form basis. These directives would be generated server side by the DBIC converter, which would need an update to add them basic on inspecting the Schema.

Here's what the it might look like on the Perl side, to create a type like the one above:

use GraphQL::Directive;
my $directive_length = GraphQL::Directive->new(
  name => 'length',
  args => { max => { type => 'Int!'}, min => {type=>'Int!'} },
  description =>  'allowed lengths for the string',
  locations => ['FIELD'],
);

my $directive_unique = GraphQL::Directive->new(
  name => 'unique',
  description =>  'Must be unique in the DB',
  locations => ['FIELD'],
);

# Both $directive_unique and $directive_length added to $graphql_schema

use GraphQL::Type::Object;
my $user_type = GraphQL::Type::Object->new(
  name => 'User',  
  fields => {
   first => { 
      type => "String!",
      directives => [
        { name => 'length', arguments => { max=>50} },
      ],
    },
    last => { 
      type => "String!",
      directives => [
        { name => 'length', arguments => { max=>50} },
      ],
    },
    user_name => { 
      type => "String!",
      directives => [
        { name => 'length', arguments => { max=>50, min=>3} },
        { name => 'unique' },
      ],
    },
  }
);

jjn1056 avatar Jun 12 '19 15:06 jjn1056

To avoid doubt, this does add clarity. However, it still doesn't show any actual queries, which would be run against the API. Do I understand right that the verification of uniqueness would be run on a different service and not in the GraphQL service itself?

mohawk2 avatar Jun 12 '19 15:06 mohawk2

Yes for my purposes the FE has its own API for the 'unique' example. . However I hope to be able to convince them to switch to GraphQL and we can kill that stand alone API. For example I think the uniqueness check could be done in Graphql with something like:

(what the FE teams POSTs)

{
  user(username:"example_username") {
    id
  }
}

(What the GraphQL API returns when the name isn't unique)

{
  "data": {
    "user": [
      {
        "id": "100"
      },
    ]
  }
}

(What the GraphQL API returns when the name IS unique)

{
  "data": {
    "user": [
    ]
  }
}

There's probably a better API we could imagine but this is one simple

jjn1056 avatar Jun 12 '19 15:06 jjn1056

That sounds like a security hole since bad people could use the API (GraphQL or otherwise) to figure out what usernames to try brute-forcing.

Certainly the idea of having programmatically created types, and also SDL ones, have these directives, is a good idea. What is done with that afterwards can be a problem for another day. I will ponder.

mohawk2 avatar Jun 12 '19 15:06 mohawk2

@mohawk2 I think that could be another reason the FE team wanted its own server for some of these checks. They have a type of authorization on that server that uses a single use key or something so that only they are supposed to be able to hit it. But like I said its just something off the top of my head.

jjn1056 avatar Jun 12 '19 15:06 jjn1056