Giter VIP home page Giter VIP logo

Comments (21)

chrissm79 avatar chrissm79 commented on July 19, 2024 3

@hailwood A lot of great questions here and I'll try to get to them after work!

Regarding the following question:

How would I allow a user to only query certain fields or relations in a query based on a role (assume $user->role returns 'admin', 'user', or 'wholesaler') without breaking eager loading.

This sounds like a great spot for a directive! So let's create one real quick:

namespace App\Http\GraphQL\Directives;

use Nuwave\Lighthouse\Support\Traits\HandlesDirectives;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Schema\Values\FieldValue;

class RoleDirective implements FieldMiddleware
{
    use HandlesDirectives;

    /**
     * Name of the directive.
     *
     * @return string
     */
    public function name()
    {
        return 'role';
    }

    /**
     * Resolve the field directive.
     *
     * @param FieldValue $value
     *
     * @return FieldValue
     */
    public function handleField(FieldValue $value)
    {
        $resolver = $value->getResolver();
        $roles = $this->directiveArgValue(
            $this->fieldDirective($value->getField(), $this->name()),
            'includes',
            []
        );

        return $value->setResolver(function ($root, $args, $context, $info) use ($roles, $resolver) {
            if (!in_array($context->user->role, $roles)) {
                return null;
            }

            return $resolver($root, $args, $context, $info);
        });
    }
}

And then place it in our schema:

type Product {
    price: Float
    wholesale_price: Float @role(includes: ["admin"])
    orders: [Order] @role(includes: ["admin", "wholesaler"])
}

In the directive, we're first getting a reference to the original resolver (which in this example handles either returns the wholesale_price or returns the hasMany relationship for orders). We're then running a check to see if the user (provided by the $context) has a role that's listed in the schema and if not it returns null, otherwise it will call the original resolver and everything will continue down the chain as normal!

from lighthouse.

chrissm79 avatar chrissm79 commented on July 19, 2024 2

Alright, next question

Question:
How would I define an external validation rule provider.

With the current beta (v2.1-beta.9), the @validate directive can be used on both arguments (w/ rules) or a field (w/ a validator). What you're asking about is the perfect use-case for a validator, but just as a refresher, here's how you would define rules for simple arguments:

extend type Mutation {
  createUser(
    name: String @validate(rules: ["required", "min:5"])
    email: String @validate(rules: ["required", "email", "unique:users,email"])
  ): User
}

This is great for mutations that have flat arguments. However, you'll likely run into scenarios where your mutation requires nested or more complex data like so:

input OrderAddressInput {
  address: String
  city: String
  state: String
  zip: String
}

extend type Mutation {
  placeOrder(
    products: [Int]
    coupons: [Int]
    different_shipping: Boolean
    shipping_address: OrderAddressInput
    billing_address: OrderAddressInput
  ): Post @validate(validator: "App\\Http\\GraphQL\\Validators\\PlaceOrderValidator")
}

So all we need to do now is create our validator class. I originally wanted to re-purpose the RequestForm provided by Laravel but it's not very straightforward without passing it through the controller so Lighthouse provides a Validator you can extend:

namespace App\Http\GraphQL\Validators\Form;

use Nuwave\Lighthouse\Support\Validator\Validator;

class PlaceOrderValidator extends Validator
{
    /**
     * Get rules for field.
     *
     * @return array
     */
    protected function rules()
    {
        $rules = [
            // ...
            'shipping_address.address' => ['required']
            // ...
            'billing_address.address' => ['required_if:different_shipping:1']
        ];
    }

    /**
     * Get validator messages.
     *
     * @return array
     */
    protected function messages()
    {
        return [];
    }
}

from lighthouse.

panchhithakkar avatar panchhithakkar commented on July 19, 2024 1

Hey @chrissm79 , thank you so much for your reply, actually I did have the '\', somehow did not show up in the copy paste in my previous comment, really sorry for that confusion, none the less, I was able to find out the root cause: I was using lighthouse V2 and it does not come with the WhereFilterDirective.php and hence my @where was not being recognized in the schema. I have upgraded to V2.1 and it has all the directives code in it, so my @where issue is solved and my schema works fine now.

I have started to get more understanding with each issue that I encounter and was able to solve the Where directive issue, so just deleted the previous comment with the stack trace for error. Again thanks a ton to guide me through, till here, it means a lot to me.

I still have one question, if I want to use "and" or "or" operator on my where clauses, how could I achieve that, Please help to guide me on how should I move forward from here. I mean I can have multiple arguments with a boolean operator arguments and a resolver to apply the boolean on those, but is it the right way, or could I achieve it by some other way as well. I have tried to show the schema as below.

 `type Query{
     systemsAnd(node_count: Int @where(operator: ">="), psu: int, boolean: String): [System!]! @paginate(model: "App\\System" scopes: ["filterByAnd"]) `

Thank you so much for all your support and efforts.

from lighthouse.

hailwood avatar hailwood commented on July 19, 2024

Question:
How would I allow a user to only update certain fields in a mutation based on a role (assume $user->role returns 'admin' or 'user').

Example Use case:
A subscription, which has a user editable title.
The user should only be able to update the name of the subscription, the admin should be able to update everything including a next_bill_on field which we definitely don't want a normal user to be able to edit.

Answer: provided by @chrissm79
This one is a bit difficult because there's several ways to attack this problem.

First, you could just extract the arguments you need based on the user's role inside the resolver function. Not the most elegant solution since you'd have to do that in multiple places if you need to reuse that logic.

Second, you could use the @can directive to check a policy on the authenticated user. You would need to create 2 mutations, but you could use the same resolver for both (this is probably the way I'd go):

extend type Mutation {
  updateSubscription(
    account: Int
  ): Subscription @field(resolver: "App\\Http\\GraphQL\\Mutations\\SubscriptionMutator@update")

  updateSubscriptionBilling(
    account: Int
    next_bill_on: String
  ) Subscription 
    @field(resolver: "App\\Http\\GraphQL\\Mutations\\SubscriptionMutator@update")
    @can(if: "updateBilling", model: "App\\Subscription")
}

Third, you could create another schema to handle a different endpoint (such as /admin/graphql) and use an environment variable to point to the correct entry point in the Lighthouse config. I personally try to avoid multiple schemas at all costs, but this is a scenario where I could see the benefits. In your admin schema, you could import the base schema and then extend your Mutation type to add some additional fields that regular user's wouldn't see.

from lighthouse.

hailwood avatar hailwood commented on July 19, 2024

Question:
How would I define an external validation rule provider.

Example Use case
the simplest example I can see right now is for editing a post, assuming the slug needs to be unique across all posts, I need to be able to provide the id field from the arguments to the ignore section.

Or conditionally adding rules based on whether another field is present.

Answer: provided by @chrissm79
With the current beta (v2.1-beta.9), the @Validate directive can be used on both arguments (w/ rules) or a field (w/ a validator). What you're asking about is the perfect use-case for a validator, but just as a refresher, here's how you would define rules for simple arguments:

extend type Mutation {
  createUser(
    name: String @validate(rules: ["required", "min:5"])
    email: String @validate(rules: ["required", "email", "unique:users,email"])
  ): User
}

This is great for mutations that have flat arguments. However, you'll likely run into scenarios where your mutation requires nested or more complex data like so:

input OrderAddressInput {
  address: String
  city: String
  state: String
  zip: String
}

extend type Mutation {
  placeOrder(
    products: [Int]
    coupons: [Int]
    different_shipping: Boolean
    shipping_address: OrderAddressInput
    billing_address: OrderAddressInput
  ): Post @validate(validator: "App\\Http\\GraphQL\\Validators\\PlaceOrderValidator")
}

So all we need to do now is create our validator class. I originally wanted to re-purpose the RequestForm provided by Laravel but it's not very straightforward without passing it through the controller so Lighthouse provides a Validator you can extend:

namespace App\Http\GraphQL\Validators\Form;

use Nuwave\Lighthouse\Support\Validator\Validator;

class PlaceOrderValidator extends Validator
{
    /**
     * Get rules for field.
     *
     * @return array
     */
    protected function rules()
    {
        $rules = [
            // ...
            'shipping_address.address' => ['required']
            // ...
            'billing_address.address' => ['required_if:different_shipping:1']
        ];
    }

    /**
     * Get validator messages.
     *
     * @return array
     */
    protected function messages()
    {
        return [];
    }
}

from lighthouse.

hailwood avatar hailwood commented on July 19, 2024

Question:
How would I allow a user to only query certain fields or relations in a query based on a role (assume $user->role returns 'admin', 'user', or 'wholesaler') without breaking eager loading.

Example Use case:
An admin should be able to query for all products, and all sales related to that product.
A normal user should only be able to query for the product and should not be able to see sales.

Or, a wholesaler should be able to query for the wholesale_price of a product, a normal user should be returned null if they ask for this field.

Answer: provided by @chrissm79
This sounds like a great spot for a directive! So let's create one real quick:

namespace App\Http\GraphQL\Directives;

use Nuwave\Lighthouse\Support\Traits\HandlesDirectives;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Schema\Values\FieldValue;

class RoleDirective implements FieldMiddleware
{
    use HandlesDirectives;

    /**
     * Name of the directive.
     *
     * @return string
     */
    public function name()
    {
        return 'role';
    }

    /**
     * Resolve the field directive.
     *
     * @param FieldValue $value
     *
     * @return FieldValue
     */
    public function handleField(FieldValue $value)
    {
        $resolver = $value->getResolver();
        $roles = $this->directiveArgValue(
            $this->fieldDirective($value->getField(), $this->name()),
            'includes',
            []
        );

        return $value->setResolver(function ($root, $args, $context, $info) use ($roles, $resolver) {
            if (!in_array($context->user->role, $roles)) {
                return null;
            }

            return $resolver($root, $args, $context, $info);
        });
    }
}

And then place it in our schema:

type Product {
    price: Float
    wholesale_price: Float @role(includes: ["admin", "wholesaler"])
    orders: [Order] @role(includes: ["admin"])
}

In the directive, we're first getting a reference to the original resolver (which in this example handles either returns the wholesale_price or returns the hasMany relationship for orders). We're then running a check to see if the user (provided by the $context) has a role that's listed in the schema and if not it returns null, otherwise it will call the original resolver and everything will continue down the chain as normal!

from lighthouse.

hailwood avatar hailwood commented on July 19, 2024

Question:
How would I add additional fields to a paginated relationship? This includes ensuring these fields could be managed via a mutation, and fetched in a query.

Example Use case:
Any additional information on a pivot table e.g. Imagine we have Users and Companies which are related through a many_many relationship. On the pivot table we have an additional column role as well as the default timestamp columns created_at, updated_at.

We want to be able to gain access to this additional information when querying for users of a given company.

Answer: provided by @chrissm79

This one required a bit of an extended discussion as there are a couple of options. Please see issue #113

from lighthouse.

chrissm79 avatar chrissm79 commented on July 19, 2024

Next Question

Question:
How would I allow a user to only update certain fields in a mutation based on a role (assume $user->role returns 'admin' or 'user').

This one is a bit difficult because there's several ways to attack this problem.

First, you could just extract the arguments you need based on the user's role inside the resolver function. Not the most elegant solution since you'd have to do that in multiple places if you need to reuse that logic.

Second, you could use the @can directive to check a policy on the authenticated user. You would need to create 2 mutations, but you could use the same resolver for both (this is probably the way I'd go):

extend type Mutation {
  updateSubscription(
    account: Int
  ): Subscription @field(resolver: "App\\Http\\GraphQL\\Mutations\\SubscriptionMutator@update")

  updateSubscriptionBilling(
    account: Int
    next_bill_on: String
  ) Subscription 
    @field(resolver: "App\\Http\\GraphQL\\Mutations\\SubscriptionMutator@update")
    @con(if: "updateBilling", model: "App\\Subscription")
}

Third, you could create another schema to handle a different endpoint (such as /admin/graphql) and use an environment variable to point to the correct entry point in the Lighthouse config. I personally try to avoid multiple schemas at all costs, but this is a scenario where I could see the benefits. In you admin schema, you could import the base schema and then extend your Mutation type to add some additional fields that regular user's wouldn't see.

from lighthouse.

chrissm79 avatar chrissm79 commented on July 19, 2024

@hailwood Sorry this one took awhile! Take a look at the answer here:
#113 (comment)

from lighthouse.

hailwood avatar hailwood commented on July 19, 2024

Question:
How would I apply different middleware to different queries?

Example Use case:
Imagine we have some "catalog" type methods that list things such as categories, these should be accessible by anyone, then we have other queries e.g. listing contacts which should only be accessible by authenticated users.

Answer:
Extend the Query type with the @group directive to apply middleware to only queries listed in that extended block!

# no middleware required to query the `categories` field
type Query {
    categories: [Category!]!
}

# "auth:api" middleware required to query `contacts` field
extend type Query @group(middleware: ["auth:api"]) {
    contacts: [Contact]!
}

from lighthouse.

hailwood avatar hailwood commented on July 19, 2024

Question:
How would I filter/order paginated results?

Example Use case:
Let's say we have a list of Posts, the Posts are by a specific user so we can query them off both the User type, and the root Query type, but we want to be able to sort them alphabetically, or by created date, or want to be able to filter them by name.

Answer: provided by @chrissm79 in #72

For filtering/ordering, you can add scopes to the hasMany and pagination directives like so:

type User {
    posts(status: PostStatus): [Posts!]! @hasMany(scopes: ["filterAndOrder"])
}

type Query {
    posts(order: PostOrder): [Post!]! @pagination(scopes: ["filterByName", "orderByDate", "orderByName"]
}

These scopes reference Eloquent scopes on the referenced model. What you do in the scopes is up to you, you could handle it all in one scope (as in the User type example), or have different scopes to handle the different options (as in the Query type example).

Note: You'll get passed the args array to your scope

from lighthouse.

panchhithakkar avatar panchhithakkar commented on July 19, 2024

I am really new to using Laravel + GraphQL, I have my project set up - Lighthouse V2, with basic queries running, can you please guide me to query by a specific field I mean something like a where clause with equals or like. Thanks for all the help and support, managed till here with all your documentation and videos for Lighthouse V2. If you could guide me to the proper documentation on this. I mean I need help to setup the type Query in my schema.graphQL file.

Here is my method in Query.php :

   public function systems($root, array $args)
   {        
       if (isset($args['sku'])) {
            return \App\system::where('sku',$args['sku'])->get();
        }else{
            return \App\system::all();
        }
    }

schema.graphql

 type Query {
   systemApplications: [SystemApplication!]! @field(resolver: 
  "App\\Http\\GraphQL\\Query@systemApplications")
  systems(sku: String): [System!]! @field(resolver: "App\\Http\\GraphQL\\Query@systems")
 }

my query:

 {
      systems(sku: "AS-2123BT-HTR"){ 
          sku
          chassis_sku
       }
 }

But how can we generalize the field, I mean this query is for reading systems table by sku, but what if I want to query by name, do I need to make Query type or resolver functions for each field in my Query.php? Please help. Please ignore my formatting, using github first time to post questions.

from lighthouse.

spawnia avatar spawnia commented on July 19, 2024

Arguments in GraphQL are optional by default, so you could easily just add more arguments to the same field and just add filters to the query for the ones you provided.

type Query {
  systems(sku: String, name: String): [System!]! @field(resolver: "App\\Http\\GraphQL\\Query@systems")
}

You can also skip writing a custom resolver altogether and use a combination of the @paginate directive and the custom filter directives. Take a look at https://github.com/nuwave/lighthouse/blob/master/tests/Integration/Schema/Directives/Args/QueryFilterDirectiveTest.php

from lighthouse.

panchhithakkar avatar panchhithakkar commented on July 19, 2024

@spawnia thanks a ton for the reply , those test cases are in too much detail, really helpful for a rookie like me. The issue I have now is that, none of the directives like @where, @include, @search are working in my case except @paginate.

Look below:
type Query {
systems: [System!]! @paginate(model: "App\System")
}

image

But when I try @paginate with the @where directive like below, it throws error.

type Query {
systems(psu: Int @where(operator: ">")): [System!]! @paginate(model: "App\System")
}

image

Am I using the wrong syntax or something else is missing? I am using Lighthouse V2, can you please help/advise.

from lighthouse.

panchhithakkar avatar panchhithakkar commented on July 19, 2024

I have issues using any type of directives like @where, @Neq, @eq as I just described in my previous comment with the screenshots, I just get error when GraphiQL tries to read the schema.graph to show the shema, do I need to activate the directives or anything that I am really missing, my questions may be too basic but I am kind of really struggling to get there for directives, any help would really be appreciated. Thanks in advance.

from lighthouse.

chrissm79 avatar chrissm79 commented on July 19, 2024

@panchhithakkar So the first thing I see is that you need to add double slashes to your schema like so:

type Query {
    # the `model` argument should have double slashes for the namespace
    systems(psu: Int @where(operator: ">")): [System!]! @paginate(model: "App\\System")
}

Other than that your schema look fine and I tested this out locally and everything seems to be working. If you're still getting the error (after adding double slashes) can you do me a favor and open up the developer tools in your browser and go to your network tab to take a look at the error that you're getting when you try to load the schema?

from lighthouse.

spawnia avatar spawnia commented on July 19, 2024

I really like the format of these "How would i" questions. Would be nice to get them worked in to the docs.

@hailwood you could really help improve the docs by adding some those in to the https://github.com/nuwave/lighthouse-docs repo

If you need help on getting started there, feel free to hit us up on Slack

from lighthouse.

panchhithakkar avatar panchhithakkar commented on July 19, 2024

I have a schema where my system table has a many to many relationship with applications table and I maintain the data in a third table system_application. I use hasmany and belongsToMany to achieve the relationship between them and it works out well while pulling data.

This is how my schema.graphql looks like now:

      type query{
      systems ( family: [String] @in(key: "product_family"),  drive: [Int] ): [System!]! 
         @paginate(model: "App\\System" scopes: ["filterByArgs"])
      }

     type SystemApplication{
        id: ID!
        name: String
        description: String
     }
     type SystemApplication1 {
          sku: String 
          drive: Int
         chassis_sku: String
         product_family: String
     }
     type Application{
           systems: [SystemApplication1!]! @hasMany(type: "paginator", relation: "systems")
           id: ID!
           name: String
          description: String   
     }
   type System {
         applications: [SystemApplication!]! @hasMany(type: "default", relation: "applications")
         sku: String 
         drive: Int
         chassis_sku: String
         product_family: String
    } 

I am using a mix of filter directives and scope in my type query. This is my scope function in system.php

  public function scopeFilterByArgs($query, array $args){              
        $drive=array_get($args,'drive');
        $dimm=array_get($args,'dimm');
        $cputype=array_get($args,'cputype');                  
        
       return $query->when(sizeOf($args), function($q) 
       use($drive, $dimm, $orderBy, $orderByDirection){  
             
           if(!empty($drive)){
               $q->WhereIn('drive_size_25', array_flatten($drive));
               $q->orWhereIn('drive_size_35', array_flatten($drive));
           }
                       
        }); 

I basically want to have one more argument for systems query type - application name and retrieve systems data. But if I have that argument, how to handle it, if I handle it in the scope function, I need to have a table join between the two tables and ten include the select block as well but then that will actually mess up my query for those I am using directives for, any help or guidance would be highly appreciated. Thank you so much.

from lighthouse.

oschade avatar oschade commented on July 19, 2024

Question:
How would I generate content or modify given content on the server while a mutation.

Example Use case:
I want to generate a random slug for a page.


Edit by @spawnia

Answer:

You can use a custom resolver to do anything you want with a mutation.

If you want to leverage an existing resolver and just add a side effect to it, you can also fire an event with the @event directive.

from lighthouse.

spawnia avatar spawnia commented on July 19, 2024

Closing this issue, as it become less and less useful as it grows larger and gets outdated. Happily accepting PR's to the docs that add a guide for some of the issues in here.

New questions can be added as seperate issues or asked in Slack. Thanks everybody for participating.

from lighthouse.

jampack avatar jampack commented on July 19, 2024

Any example for Union custom resolver?

from lighthouse.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.