Closed hailwood closed 5 years ago
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.
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 [];
}
}
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!
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
@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!
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 [];
}
}
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.
@hailwood Sorry this one took awhile! Take a look at the answer here: https://github.com/nuwave/lighthouse/issues/113#issuecomment-389010479
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]!
}
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
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.
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
@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") }
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") }
Am I using the wrong syntax or something else is missing? I am using Lighthouse V2, can you please help/advise.
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.
@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?
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.
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
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.
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.
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.
Any example for Union custom resolver?
This issue exists as a bit of a dumping ground of "How would I do this" questions that we could potentially answer and add to the docs in a specific section.
As any answers are posted the original asker should add them to their question to keep things easy to read. I've provided a few below to give us some structure.