GithubHelp home page GithubHelp logo

studio-net / laravel-graphql Goto Github PK

View Code? Open in Web Editor NEW
56.0 10.0 5.0 416 KB

GraphQL implementation with power of Laravel

Home Page: https://github.com/studio-net/laravel-graphql/

License: MIT License

PHP 100.00%
graphql laravel eloquent entity mutations query transformer generator

laravel-graphql's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-graphql's Issues

Pipes - missing documentation + bug

It would be nice, if using of pipes in definition would be documented. I also asked you, if it is possible to use pipes for authentication or other type of permission check is possible. You replied with "no", but actually it is possible. By the way, HTTP Middlewares in laravel, like auth middleware, works just the same.

I thing I got the idea of pipes, it is really nice! I also noticed, that it is generally possible to define pipe in following format: Class\Name\Of\Pipe:argument,another argument. So pipline can parse this string format, and get the class of pipe for creating it from container, and passes to the handle() method some addition parameters, in the example above: argument and another argument as 4th and 5th argument. It works well with pipeline code, but you also use pipes for gql fields generation... and this part of code dont accept this string format. May we fix it? As I would like to add a pipe for checking user role.

I'll do a merge request, so you can see what I mean... When we merge fix-bug, I can also write a small doc for pipe usage

Deleting of many2many relations

Currently I am working a lot with morphed many2many relationships. They gets stored and linked perfectly, but if I want to remove related object, I have to call separately delete mutation for those related objects... For one - it is ok, but when I got many, it gets a little cumbersome.

So here is an Idea - may be can we add an additional field for those relation inputs, like _delete: boolean, so that unlinked objects will be directly removed?

For exampe: I have an user with multiple locations (e.g. with IDs 3 and 4). Then I would like to store a user with locations like this:

mutation {
   user(id: 1, with: {
      locations: [
        {id: 3, _delete: true}, {id: 4, _delete: true}
      ]}
   ) {
      id
   }
}

So on execution locations with IDs 3 and 4 will be also deleted.

If it is too strange, then may be can we add ability to remove multiple items with one mutation call on dropTransformer?

Backtick sql bug when using filter transformer?

When trying to run a query like query={users(filter:{id:"1"}){id,name}} it yields the error in MySql:

Column not found: 1054 Unknown column 'LOWER(id)' in 'where clause' (SQL: select * from `users` where `LOWER(id)` = 1)"

shouldn't this be LOWER(`id`)?

How to filter relations

Thanks for making this great package. It makes GraphQL even easier! However, I seem to have ran in to a problem. I was hoping I could make a filter function like this in my AlertDefinition:

    /**
     * @return array
     */
    public function getFilterable()
    {
        return [
            'active' => function (Builder $builder, $value) {
                if (!is_bool($value)) {
                    throw new InvalidArgumentException('Active filter must be of type boolean');
                }

                return $builder->whereActive($value);
	    },
        ];
    }

And in my EndpointDefinition I would call the relation like this:

    /**
     * Which fields are queryable ?
     *
     * @return array
     */
    public function getFetchable()
    {
        return [
            'id'     => Type::id(),
            'alerts' => Type::listOf(\GraphQL::type('alert')),
        ];
    }

Now unfortunately, this does not work. The filter will be applied to the root alert query, but it will not allow me to filter like this:

query {
  endpoints {
   id,
   name,

   alerts (filter: {active: true}) {
     id
     created_at
   }
  }
}

Is there anyway to achieve this "automagically" instead of having to use a custom resolver? Because that would mean that if I would like to apply the same filter on the root alert query, I would either have to write the logic twice, or do some class extending / traits magic which seems rather cumbersome.

I suppose it would be ideal if something like this would be possible:

    /**
     * Which fields are queryable ?
     *
     * @return array
     */
    public function getFetchable()
    {
        return [
            'id'     => Type::id(),
            'alerts' => AlertDefinition::class,
        ];
    }

(How) can something like this be achieved?

Error reporting bug

I currently have sometimes problems, if I throw Error in my code. I implemented a custom scalar type for my schema: ObfId. I use it to obfuscate ID in graphql (on result and on input). I have some checks on parsing a value, e.g. in parseLiteral method, that parse value of this type in graphql-query:

use GraphQL\Error\Error;
use GraphQL\Language\AST\IntValueNode;
use Jenssegers\Optimus\Optimus;
...
function ($valueNode) {
    if (!$valueNode instanceof IntValueNode) {
        throw new Error('Query error: Can only parse integer got: '.$valueNode->kind, [$valueNode]);
    }
    /** @var Optimus $optimus */
    $optimus = resolve(Optimus::class);

    return $optimus->decode($valueNode->value);
}

So I check, if given value is an Integer, and if not, I want to throw an error. So I expect to get from server something like

{
    "errors": [
        {
            "message": "Query error: Can only parse integer got: StringValue",
            "category": "graphql",
            "locations": [
                {
                    "line": 2,
                    "column": 11
                }
            ]
        }
    ]
}

But I get following (I shortened it):

<!DOCTYPE html><!--


Symfony\Component\Debug\Exception\FatalThrowableError: Argument 1 passed to StudioNet\GraphQL\GraphQL::formatGraphQLException() must implement interface Throwable, null given, called in /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQL.php on line 140 in file /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQL.php on line 152
Stack trace:
  1. Symfony\Component\Debug\Exception\FatalThrowableError-&gt;() /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQL.php:152
  2. StudioNet\GraphQL\GraphQL-&gt;formatGraphQLException() /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQL.php:140
  3. StudioNet\GraphQL\GraphQL-&gt;StudioNet\GraphQL\{closure}() [internal]:0
  4. array_map() /var/www/my-headhunter/server/vendor/webonyx/graphql-php/src/Executor/ExecutionResult.php:130
  5. GraphQL\Executor\ExecutionResult-&gt;GraphQL\Executor\{closure}() /var/www/my-headhunter/server/vendor/webonyx/graphql-php/src/Executor/ExecutionResult.php:134
  6. GraphQL\Executor\ExecutionResult-&gt;toArray() /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQL.php:142
  7. StudioNet\GraphQL\GraphQL-&gt;execute() /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQLController.php:96
  8. StudioNet\GraphQL\GraphQLController-&gt;executeQuery() /var/www/my-headhunter/server/vendor/studio-net/laravel-graphql/src/GraphQLController.php:48
  9. StudioNet\GraphQL\GraphQLController-&gt;query() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Controller.php:54
 10. call_user_func_array() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Controller.php:54
 11. Illuminate\Routing\Controller-&gt;callAction() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php:45
 12. Illuminate\Routing\ControllerDispatcher-&gt;dispatch() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Route.php:212
 13. Illuminate\Routing\Route-&gt;runController() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Route.php:169
 14. Illuminate\Routing\Route-&gt;run() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Router.php:665
 15. Illuminate\Routing\Router-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:30
 16. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:104
 17. Illuminate\Pipeline\Pipeline-&gt;then() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Router.php:667
 18. Illuminate\Routing\Router-&gt;runRouteWithinStack() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Router.php:642
 19. Illuminate\Routing\Router-&gt;runRoute() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Router.php:608
 20. Illuminate\Routing\Router-&gt;dispatchToRoute() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Router.php:597
 21. Illuminate\Routing\Router-&gt;dispatch() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:176
 22. Illuminate\Foundation\Http\Kernel-&gt;Illuminate\Foundation\Http\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:30
 23. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/fideloper/proxy/src/TrustProxies.php:57
 24. Fideloper\Proxy\TrustProxies-&gt;handle() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
 25. Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
 26. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:31
 27. Illuminate\Foundation\Http\Middleware\TransformsRequest-&gt;handle() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
 28. Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
 29. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:31
 30. Illuminate\Foundation\Http\Middleware\TransformsRequest-&gt;handle() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
 31. Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
 32. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php:27
 33. Illuminate\Foundation\Http\Middleware\ValidatePostSize-&gt;handle() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
 34. Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
 35. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php:62
 36. Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode-&gt;handle() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
 37. Illuminate\Pipeline\Pipeline-&gt;Illuminate\Pipeline\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
 38. Illuminate\Routing\Pipeline-&gt;Illuminate\Routing\{closure}() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:104
 39. Illuminate\Pipeline\Pipeline-&gt;then() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:151
 40. Illuminate\Foundation\Http\Kernel-&gt;sendRequestThroughRouter() /var/www/my-headhunter/server/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:116
 41. Illuminate\Foundation\Http\Kernel-&gt;handle() /var/www/my-headhunter/server/public/index.php:55
...

So I dived into StudioNet\GraphQL\GraphQL::formatGraphQLException() - on exception, it tries to get previous exception, which is not defined, as I threw a simple exception, without to define what previous exception was, as there is currently no previous exception...

I guess there is a point, why $error->getPrevious() is used, but can we extend it with a check? Like check, if previous exception exists, if yes, format previous, if not format current one.

Or am I throwing wrong type of Error?

Bug with nested empty value

There is a bug when a nested request is sent and a belongsTo property is set to null.

mutation ($propertyId: ID!, $property: PropertyInput!) {
  updateProperty: property(id: $propertyId, with: $property) {
    id,
    owner {
      id
    }
  }
}

With values

{
  "propertyId": "1",
  "property": {
    "owner": null
  }
}

doesn't work,

Undefined column: 7 ERREUR: la colonne « owner » de la relation « properties » n'existe pas

But it works fine when filled with data

Convert InputObjectType to Array type

StudioNet\GraphQL\Generator\Query\NodesEloquentGenerator::getFilterType() method is using InputObjectType type and each field within has a String type, that prevent us to do following :

entities (filter : {"reference": {"or" : ["20%", "30%", "(gt) 5000"]}}) {
   # ...
}

The type should be our scalar Array type. I prefer using array each time instead of string.

Implementing kind of auth middleware

Hi. I decided to discuss it with you... I wish a possibility simply to define, if some queries are protected. As I used rebing/graphql-laravel before, I am very inspired how they solved it there.

As first - there are multiple possibilities, how it could be used (all situations are already implemented in rebing lib)

Schema-scoped

This seems to be the simplest solution. We can define multiple schemas. As option, it would be nice to define middlewares for schemas in config. Maybe something like this:

[...
    'schema' => [
        'default'     => 'default',
        'definitions' => [
            'default' => [
                'query'    => [...],
                'mutation' => [...],
                'middleware' => ['auth']
            ]
        ]
    ],
]

OR: as we speak about authorization, just define 'authorization' => true

It looks simple though, I don't think it is very useful. In my last application I was using apollo-client with some extra plugins. We decided to use single schema, but if user is logged in - then we simple added authorisation header. If user is allowed to query particular data was decided on server, depending on authorisation header. Anyway I think it is quite uncomfortable to use multiple schemas on client side, and each time to decide, which should be used... So in my opinion this option make simply no sense...

Query/Mutation-scoped

May be we could implement something like authorize() Method in query/mutation, that should return a boolean. If false is returned, then GraphQL throws an error (like 'Unauthorized'). Usage could then look like this (same as in rebing lib)

use Auth;

class UsersQuery extends Query
{
    public function authorize(array $args)
    {
        // true, if logged in
        return !Auth::guest();
    }
    ...
}

Field-scoped

This is not as much as middleware, but as idea - hide fields, that user may not access. This is called privacy in rebing lib (see here). This seems to be very helpful, but I don't know, if it is so in practice.

What do you think? Should we implement something of this?

Is error reporting broken?

When some of my code throws an \Error or \Exception, the error I get back includes a "location", but no indication of where the error occurred — no file, or class name, or trace.

Is this by design? Could we do something like this in formatError()?

if (!empty($prev)) {
	if ($prev instanceof ValidationError) {
		$error['validation'] = $prev->getValidatorMessages()->toArray();
	} else if ($prev instanceof \Throwable) {
	    // Throw in the trace so one can know where an exception occurred
            $error['trace'] = explode("\n", $prev->getTraceAsString());
        }
}

That would give us a nicely formatted trace back to where the original \Throwable was thrown, so the error is findable (though it should probably be switched based on env(APP_DEBUG))

N+1 Problem with ListTransformers

Hi there! Thank you very much for this awesome package! I am new to GraphQL and tried before a package from rebing, but you package seems to be more fresh and your features are simple crazy awesome!

When I tried your library, it was important to me, that all relationships loads eager. As you provide transformers, I didn't even need to write a query for testing this out - nice! BUT exactly there I found a bug.

I would like to fix it, but I don't feel much confidence with your code, as I see at it since 1 hour. So I just describe the problem ;)

In file StudioNet\GraphQL\Support\Transformer\Transformer.php in method getResolverCallable() you tries to guess the relations with $this->guessWithRelations($definition, $fields). For ViewTransformation (and all other mutation-transformations) it works perfectly. Relations are guessed correctly, as fields at the root contains exactly the structure of queried type.

But to query data via ListTransformer, we have to use query like this:

{
   items {...}
}

So in getResolverCallable() the variable $fields contains that, what was given (same as in example).
When you then call $this->guessWithRelations($definition, $fields), variable $fields contains wrong attributes, better saying it contains correct attributes, but inside items{...}. So the guessWithRelations() guesses, there are no relations at all, because it checks only items field.

I know, you have a TODO there (Improve this checker). If you wish, I could try to improve it.

P.s. Child-relations are also not guessed.

__typename field on Input types

Hi, I just installed a plugin on my PhpStorm: JS GraphQL. It has a great support for working with graphql. When I parsed my schema with this plugin, it threw me an error:

Error: Name "__typename" must not begin with "__", which is reserved by GraphQL introspection.

So my question - Is there any sense in appending __typename field of type "string" to all input types for definitions? I didn't see this in other gql schemas before.

Can we get rid of it? I also didn't find any example for creation Input types with __typename field.

Truely nested relation resolvers

On defining mutable fields, it is currently possible to simply define which relation should be updated. Input type of this relation is automatically used, as input type of the defined field.

But what I faced today - for the relations transformation there is pretty simple code, that is not using something from definitions of this relation at all.

What I was trying: Let's say we have User and Location models. User can have many Locations. Location has a custom input "shape", which could be either a polygon, or simple point with lat/long and radius in meters, for creation circle on the map. On the server side this field is always converted so, that DB stores only polygons.

If I create Location directly, my custom input works properly. But when I am using nested mutation, it doesn't work, because relations transformation is processed on its own, and has nothing in common with definition.

So it would be really nice, when on defining custom input in definition, this custom input would be respected also on nested mutations (relations).

I did a quick look on code, and didn't find any quick solution. So what du you think? As idea - good/bad? And if its good, how would you like to implement it? May be if you could give me some advices, I could try to code it on my own

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.