GithubHelp home page GithubHelp logo

laravel's Introduction

Tests

JSON:API for Web Artisans

Implement feature-rich JSON:API compliant APIs in your Laravel applications. Build your next standards-compliant API today.

Why use JSON:API and Laravel JSON:API?

Great question! Here's some reasons from this excellent article by Denisa Halmaghi:

Why Use JSON:API?

  • Standardised, consistent APIs.
  • Feature rich - some of which are: sparse fieldsets (only fetch the fields you need), filtering, sorting, pagination, eager loading for relationships (includes, which solve the N+1 problem).
  • Easy to understand.

Why use Laravel JSON:API?

  • Saves a lot of development time.
  • Highly maintainable code.
  • Great, extensive documentation.
  • Strong conventions, but also highly customisable.
  • Makes use of native Laravel features such as policies and form requests to make the shift easier for developers.
  • Beautiful, expressive Nova-style schemas.
  • Fully testable via expressive test helpers.
class PostSchema extends Schema
{

    /**
     * The model the schema corresponds to.
     *
     * @var string
     */
    public static string $model = Post::class;

    /**
     * The maximum include path depth.
     *
     * @var int
     */
    protected int $maxDepth = 3;

    /**
     * Get the resource fields.
     *
     * @return array
     */
    public function fields(): array
    {
        return [
            ID::make(),
            BelongsTo::make('author')->type('users')->readOnly(),
            HasMany::make('comments')->readOnly(),
            Str::make('content'),
            DateTime::make('createdAt')->sortable()->readOnly(),
            DateTime::make('publishedAt')->sortable(),
            Str::make('slug'),
            BelongsToMany::make('tags'),
            Str::make('title')->sortable(),
            DateTime::make('updatedAt')->sortable()->readOnly(),
        ];
    }

    /**
     * Get the resource filters.
     *
     * @return array
     */
    public function filters(): array
    {
        return [
            WhereIdIn::make($this),
            WhereIn::make('author', 'author_id'),
        ];
    }

    /**
     * Get the resource paginator.
     *
     * @return Paginator|null
     */
    public function pagination(): ?Paginator
    {
        return PagePagination::make();
    }
}

Documentation

See our website, laraveljsonapi.io

Tutorial

New to JSON:API and/or Laravel JSON:API? Then the Laravel JSON:API tutorial is a great way to learn!

Follow the tutorial to build a blog application with a JSON:API compliant API.

Installation

Install using Composer

composer require laravel-json-api/laravel

See our documentation for further installation instructions.

Upgrading

When upgrading you typically want to upgrade this package and all our related packages. This is the recommended way:

composer require laravel-json-api/laravel --no-update
composer require laravel-json-api/testing --dev --no-update
composer up "laravel-json-api/*" cloudcreativity/json-api-testing

Example Application

To view an example Laravel application that uses this package, see the Tutorial Application.

License

Laravel JSON:API is open-sourced software licensed under the MIT License.

laravel's People

Contributors

dacercoolex avatar helmroos avatar jazo avatar lindyhopchris avatar prinsfrank avatar robchatloop avatar villfa avatar x-coder264 avatar

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  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  avatar  avatar  avatar  avatar  avatar  avatar

laravel's Issues

Dynamic schema?

I'm very new to this package, so I apologise if I get terminology/concepts wrong :-)

We would like to build an API where we can make "plugins" that add extra functionality. However, this would mean the plugins would need to add additional relationships to the core schema ideally.

e.g. in the core we may have "products" and in a plugin we might want to add "reviews" that relate to "products". And we'd want to be able to request a product with reviews included.

Is this doable?

Got a trouble with 500 error using sparse fields

Can't use sparse fields option of specificaction (https://jsonapi.org/format/#fetching-sparse-fieldsets) like this:
GET api/v1/posts?include=user&fields[posts]=title, I am getting 500 error end an exception: "message": "Argument 1 passed to LaravelJsonApi\\Core\\Query\\FieldSets::LaravelJsonApi\\Core\\Query\\{closure}() must be of the type array, string given", "exception": "TypeError", "file": "/var/www/html/vendor/laravel-json-api/core/src/Core/Query/FieldSets.php", "line": 74,

Dependency injection in server classes - problems and improvements

Just calling JsonApiRoute::server() to register the routes produces a lot of side effects as it immediately resolves the server instance which resolves recursively all of the server dependencies which especially impacts the testing.

My use-case is that I have a client courses resource for which the index action needs to be automatically scoped so that the client can see only the courses that it has access to. The client in this case is the OAuth2 client which authenticates to the app (I'm using Passport for that). So in order to do this I'm adding a scope in the serving method of the server:

    public function serving(): void
    {
        ClientCourse::addGlobalScope($this->oAuthClientScope);
    }

And the scope code looks like this:

<?php

declare(strict_types=1);

namespace App\JsonApi\V1\ClientCourses;

use Illuminate\Contracts\Config\Repository;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Laravel\Passport\Client as PassportClient;
use Laravel\Passport\Guards\TokenGuard;
use LogicException;

final class OAuthClientScope implements Scope
{
    private $request;
    private TokenGuard $tokenGuard;
    private Repository $config;

    public function __construct(
        callable $request,
        TokenGuard $tokenGuard,
        Repository $config
    ) {
        $this->request = $request;
        $this->tokenGuard = $tokenGuard;
        $this->config = $config;
    }

    public function apply(Builder $builder, Model $model)
    {
        /** @var PassportClient|null $client */
        $client = $this->tokenGuard->client(call_user_func($this->request));

        if (null === $client) {
            throw new LogicException('This should not have happened.');
        }

        // there is actually some more code here that maps the OAuth client model (oauth_clients table)
        // to the application client model (clients table), but it's not relevant for the issue here

        $builder->where('client_id', '=', $client->getKey());
    }
}

The scope instance (which gets injected into the server) has a constructor dependency on the HTTP request and on the Passport Token Guard which can tell me what OAuth client is authenticated based on the access token that is sent in the request (https://github.com/laravel/passport/blob/v10.1.2/src/Guards/TokenGuard.php#L122).

The problem here is that since the server gets resolved when the routes are registered it means that at that point in time the scope instance that is injected in the server has an empty HTTP request and for some reason the server instance is cached in the server repository (https://github.com/laravel-json-api/core/blob/v1.0.0-alpha.4/src/Core/Server/ServerRepository.php#L70-L72). That means I don't get a new server instance when I do the request in my feature test (which means that the request in the scope is empty as it was resolved before the actual request in the test was made). I was able to work around this by making the request a callable (as you can see in my scope code above) instead of directly typehinting the request class. That helped a bit as the request is now resolved only when it's actually needed instead of being resolved along with the server (and I don't consider this as a proper solution, it's more of a temporary hack), but now the problem is that the token guard depends on the OAuth2 Resource server which is supposed to be mocked by Passport in my test :

$oauthClient = PassportClientFactory::new()->create();

Passport::actingAsClient($oauthClient);

The actingAsClient essentially does app()->instance(ResourceServer::class, $mock); (https://github.com/laravel/passport/blob/v10.1.2/src/Passport.php#L395), but since the resource server was also resolved before the mocked instance was set into the container in my test I have the same problem that I get the actual non mocked resource server injected into the token guard so my test explodes as the non mocked server does not return the expected client back.

Removing the caching of the server in the server repository helps to solve this specific problem (as a new server instance gets created once my feature test does the request), but it still leaves the underlying problem unresolved and that is that the server still gets resolved on route registration and all recursive dependencies with it.

This is a problem when for example typehinting \Illuminate\Contracts\Auth\Guard in the constructor of the server and I get the default guard every time there (which is the web guard) instead of the api guard which I wanted (because the auth:api middleware that is supposed to set the guard has not been run at that point in time yet). The workaround for this is to explicitly use contextual binding for the server:

use Illuminate\Contracts\Auth\Factory as AuthFactory;

        $this->app->when(Server::class)
            ->needs(Guard::class)
            ->give(static function (Container $container): Guard {
                /** @var AuthFactory $authFactory */
                $authFactory = $container->make(AuthFactory::class);
                return $authFactory->guard('api');
            });

This works, but it only works under the assumption that the only guard that will ever be able to be used with this server is the api guard which is not a valid assumption for more complex projects where there can be multiple guards that could be used for the same API routes.

So removing the caching layer in the server repository only solves part of the problem. The other part would be making the package work in a way that that the server instance is not instantiated just for route registration. This could be solved by configuring the stuff that is needed for route registration via the config (per each server) or by making the needed data for route registration available on the server via static method(s) so that a server instance is not needed and so that the server gets resolved only when the \LaravelJsonApi\Laravel\Http\Middleware\BootJsonApi middleware runs (which can be configured in the HTTP kernel to only run after the Laravel auth middleware).

Extraction of existing resource fields for validation needs refactoring

The implementation of extracting existing resource field values for validating an update request was brought in from the old package. There are two reasons why it needs refactoring:

  1. It pulls the current values from the resource class; however this class can return things like conditional attributes, or it could contain JsonSerializable objects. This would result in these values being passed to the validation rules, which could then fail. What we actually need to do is encode the values to JSON and decode them to get the actual JSON:API field values.
  2. We use the mustValidate method on the resource's relations. However, resources are now optional classes - so the mustValidate method should be moved to the schema relation field - which is a better place for it anyway.

The solution to both is probably to pass the existing model to the encoder to encode... and use the mustValidate method on the schema's relations to work out what include paths to pass in - i.e. to force the encoder to give us data members for the relationships that must be validated.

When doing this, we should encode to a JSON string, then decode... so that we get the actual JSON values for any objects that implement JsonSerializable.

It would be worth putting all this logic into a single class that is responsible for the conversion and merging with the values provided by the client. As this relates to validation the class should probably be in the laravel-json-api/validation package; and then the request classes in this package use that object to compute the validation data for an update request.

Can I get request from the middleware?

I am working on package which I believe will build auto generated api documentation. It works based on tests coverage using middleware like this:

public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        if(Config::get('app.debug'))
            $this->docDocDocService->parse($request, $response);

        return $response;
    }

I can get request and response from each test, so I have status codes, parameters, bodies, headers, etc. Now I want to get validation rules from form requests. It works fine with form requests injected to the action as a parameter, as I can see json api server requests are not injected to action, but are initializing inside the actions, like this:

$request = ResourceQuery::queryOne(
            $resourceType = $route->resourceType()
        );

I can't see any possibility to get it inside the scope of the request, do you?

Published configuration and artisan command to create server does not play well together

Published configuration includes a default server named v1:

'servers' => [
'v1' => \App\JsonApi\V1\Server::class,
],

But publishing the configuration does not create required files for that server. \App\JsonApi\V1\Server does not exist.

Artisan command to create the server fails because the server is already registered:

$ php artisan jsonapi:server v1
Server v1 is already registered in your JSON:API configuration.

Must remove the server registration in config/jsonapi.php before creating the required files with artisan command.

jsonapi:server artisan command does not register the created server in config/jsonapi.php. Registration deleted before running the command must be added again manually after running it.

Steps to reproduce

  1. Create a new laravel project with composer installer
    composer create-project laravel/laravel example-app
    cd example-app
  2. Install laravel-json-api/laravel (tested with v1.0.0-alpha.1)
    composer config minimum-stability alpha
    composer require laravel-json-api/laravel
    composer require --dev laravel-json-api/testing
  3. Publish configuration
    php artisan vendor:publish --provider="LaravelJsonApi\Laravel\ServiceProvider"
  4. Try to create server
    php artisan jsonapi:server v1

Remove need to set type and id on a resource class

At the moment, if you have a resource class e.g. PostResource, you have to set the type and id if they do not follow the conventions of dash-casing the class name and using the Model::getRouteKey() method. This duplicates configuration that is held on the Schema - i.e. the schema already knows what the type and id are.

If we always inject the schema into the resource class, then this would remove the need to set these two things because the resource could delegate to the schema.

The consequence of this is it means resources cannot be returned by a developer - as they would always need to be constructed with a schema - i.e. the resource container always needs to be used. However this isn't a problem because if the developer wants to return an arbitary resource they should be using DataResponse instead.

Clear Support for Virtual Resources

Creating "virtual" resources for an API is a common need and I'm struggling creating one using this package. Any thoughts or workarounds on this? Is seems any schema NEEDS an eloquent model which seems very limiting in some very valid use cases.

Thanks!

Scout integration

Need to work out how to integrate Scout into index queries (and to-many relationship queries). This has been requested in the previous package here:
cloudcreativity/laravel-json-api#449

And the solution should probably take into account how Nova do it (although it's a lot more complicated in our scenario because it's not just tied to an input box that searches a specific resource, it's tied to a client's index query that could contain standard filters).

Nova solution here:
https://nova.laravel.com/docs/3.0/search/scout-integration.html

Ensure specification validation checks relations are of expected type (to-one or to-many)

RE: this issue:
cloudcreativity/laravel-json-api#335

We need to ensure that the specification validator rejects a document that has the wrong type of relation - e.g. submitting a to-many for a to-one, and vice versa.

The spec validator now has access to this information via the schema classes.

I suspect I've already implemented this, but just need to double-check that it is implemented and tested before closing this issue!

Pass Router instance through to the Closure

This might be a bit unorthodox, but I think it would be beneficial to provide the passed Router object for registering actions alongside the resource routes.

Here's an example of how this could look:

JsonApiRoute::server('v1')
    ->prefix('v1')
    ->resources(function ($server, $routes) {
        // /v1/server/...
        $server->resource('servers', ServersController::class);
        $routes->post('servers/{server}/shutdown', [ServersController::class, 'performShutdown']);
    })

Implementation would be as simple as accepting a $routes variable (the Router instance for the group) as the only parameter to the group callback and passing that $routes variable as the second parameter to the resources Closure.

Add a with-count query parameter that works with the Eloquent implementation

This has long been requested for Laravel JSON:API. Part of doing the revamp was to make things like this easier to implement. Background here: cloudcreativity/laravel-json-api#166

The idea is basically to give the client control over what relationships have a count field added to their meta.
https://laravel.com/docs/8.x/eloquent-relationships#counting-related-models

Something like:

GET /api/v1/posts?with-count=comments

Would return the posts resources with a count added to the meta of the comments relationship.

For relationship endpoints, we'd need to distinquish between counting the relation that is being retrieved, and adding a count to the models within the relation. E.g:

GET /api/v1/posts/1/comments?with-count=likes

In this request, the likes refers to a relationship on the comments resource. But we may want to include the count of how many comments the post has - so we need another parameter for indicating that for relationship endpoints. E.g. ?count-related.

Note that the JSON:API spec recommends using at least one non a-z character in the query parameter name, to avoid potential collisions with future additions by the spec. So with-count, with_count and withCount would all be ok. (To follow our convention, with-count should be the default but should be configurable.)

Once we've implemented for count, it presumably will be easy to implement the other aggregate functions.

Bulk resources create/update/delete approach

I know that it's out of the specification, at least yet, but it's kind of common thing on the backend to create/update/delete more then one resource. I've found some approaches how do people do this in borders of json api in general, but I am just curious what is the best practise to do such a superstructure in a scope of this package. I am trying to rewrite controller methods in doc specified way, and which I believe is the only way here, but it feels bad, I need to break json api payload structure for this, and I dont feel where and how I can do it safely and clean. Otherwise, the only clean way to do it in the specification style is to send one request for one resource, but is it warranted to do it like that if I need to mass update 10 resources? 10 requests? Could you please provide some info about this, I don't want to build an ugly monster here.

Support polymorphic to-many relationships

Given a posts resource has a has-many relationship called media, that may contain images, audios and videos . The JSON:API resource object may look like this.

{
  "type": "posts",
  "id": "5",
  "relationships": {
    "media": {
      "data": [
        { "type": "videos", "id": "7" },
        { "type": "audios", "id": "9" },
        { "type": "images", "id": "38" },
        { "type": "videos", "id": "11" },
        { "type": "images", "id": "234" },
      ]
    }
  }
}

The package doesn't actually support this at the moment. The reason being is that there is no Eloquent relation for this. In this scenario, the Post model would look like this:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    
    public function audios()
    {
        return $this->hasMany(Audio::class, 'media');
    }
    
    public function images()
    {
        return $this->hasMany(Image::class, 'media');
    }

    public function videos()
    {
        return $this->hasMany(Video::class, 'media');
    }
}

The relations might not just be hasMany. For example, if Image models could be attached to multiple Post models, the image relation could be a belongsToMany. Likewise, if Image models could be attached to multiple different types of models, it could be a morphedByMany relation.

Merging these all into a single JSON:API relation is not currently supported.

Controller not found

First of all, thank you for the time and the package.

I need to customize some of the create and update functions so I have created a controller for the resource as the documentation says

php artisan jsonapi:controller Api/V1/BuildingController

In routes add the buildings resource

JsonApiRoute::server('v1')
    ->prefix('v1')
    ->namespace('Api\V1')
    ->resources(function ($server) {
        $server->resource('buildings');
    });

But when I go to see the routes, it say me the Controller not exists

php artisan r:l --columns=URI,name
Cannot load Xdebug - it was already loaded

   Illuminate\Contracts\Container\BindingResolutionException 

  Target class [Api\V1\BuildingController] does not exist.

  at vendor/laravel/framework/src/Illuminate/Container/Container.php:832
    828▕ 
    829▕         try {
    830▕             $reflector = new ReflectionClass($concrete);
    831▕         } catch (ReflectionException $e) {
  ➜ 832▕             throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
    833▕         }
    834▕ 
    835▕         // If the type is not instantiable, the developer is attempting to resolve
    836▕         // an abstract type such as an Interface or Abstract Class and there is

  1   [internal]:0
      Illuminate\Foundation\Console\RouteListCommand::Illuminate\Foundation\Console\{closure}(Object(Illuminate\Routing\Route))

      +13 vendor frames 
  15  [internal]:0
      Illuminate\Foundation\Console\RouteListCommand::Illuminate\Foundation\Console\{closure}(Object(Illuminate\Routing\Route))

But is here

image

If I use the JSON:API Controller work perfect

Thanks for your time.

Model not found exception should not display exception message

Seeing this for a ModelNotFoundException:

{
    "errors": [
        {
            "detail": "No query results for model [CloudCreativity\\DanceCloud\\Models\\DanceEvent].",
            "status": "404",
            "title": "Not Found"
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

Need to remove the detail member as displaying the class name is not correct.

Conditional relationships

At the moment the resource objects support conditional attributes in the attributes and meta values. We should also add support for conditional relationships.

To do this, we need to support (in the encoder) having relations wrapped in either a conditional attribute or conditional attributes object. So the syntax on the resource would be:

public function relationships($request): iterable
{
    return [
        $this->relation('author'),
        $this->when($request->user()->isAdmin(), $this->relation('publishedBy')),
        $this->mergeWhen($request->user()->isOwner(), [
            $this->relation('foo'),
            $this->relation('bar'),
        ]),
    ];
}

Probably we should rename the ConditionalAttr and ConditionalAttrs classes to ConditionalField and ConditionalFields considering we're now not just using them in attributes.

Controller before hooks need to allow the hook to return a response

For example, on a resource we wanted to use the deleting() hook to run our own deletion logic. However, there is no way to prevent the controller action method from running the standard deletion logic, because it does check for a return value from the deleting hook.

Add a meta-only response class and add `withServer` method to all response classes

At the moment we are missing a response class for a meta-only response.

We also need to add a withServer method on our response, so that you can manually set the server that should be used to encode the response. This is needed if you are using the response classes outside of the JSON:API middleware. In such scenarios, you get an error because the responses try to get the jsonapi top-level document object from the server bound in the container, which is missing if the middleware hasn't been run.

Support equivalent of Nova lenses

Nova lenses:
https://nova.laravel.com/docs/3.0/lenses/defining-lenses.html

For example, you may want to list of all your application's users sorted by their total lifetime revenue. Creating such a list may require you to join to additional tables and perform aggregate functions within the query.

We should add these - effectively they would be a read-only resource type. I.e. the table that is shown in that chapter as an example would be a JSON:API resource index route, returning a resource type that is the lens name.

Confusion over default pagination documentation

There is some conflicting information in the docs regarding pagination behavior when no page query parameters are sent by the client.

Here, it implies that requests will always be paginated by default by what is defined on the model, or by what is set as the default on the paginator:
Screen Shot 2021-02-05 at 6 08 49 PM

But here, there is a section about forcing pagination when no query parameters are set:
Screen Shot 2021-02-05 at 6 10 54 PM

The latter is the behavior I observe as being correct but the documentation of the former makes this confusing and I'm not sure what the point of it is.

Resource links should be optional

I have a custom resource for monthly reports and I have only the index action for that resource. In the controller I'm just returning new DataResponse($reports).

Response example:

{
    "jsonapi": {
        "version": "1.0"
    },
    "data": [
        {
            "type": "monthlyReports",
            "id": "13b1ac51-3eac-411a-b26c-d0a1ef296d9a",
            "attributes": {
                "date": "2021-02",
                "reportData": [
                    {
                        "name": "Foo",
                        "Sections started": 4,
                        "Sections completed": 1
                    },
                    {
                        "name": "Bar",
                        "Lessons started": 148,
                        "Lessons completed": 11
                    },
                    {
                        "name": "Baz",
                        "Sections started": 51,
                        "Sections completed": 23
                    }
                ]
            },
            "links": {
                "self": "http://lms.my-project.loc:8892/api/v1/monthly-reports/13b1ac51-3eac-411a-b26c-d0a1ef296d9a"
            }
        }
    ]
}

The problem is that I don't have the show action so there must not be any self links as they are not valid and as such they are misleading to the client (the resource ID is just \Illuminate\Support\Str::uuid()->toString()). There is no way to say on the DataResponse class that I don't want those links (as far as I can see) and if I return an empty string in the url method of the schema an UnexpectedValueException exception gets thrown in the \LaravelJsonApi\Core\Document\LinkHref constructor which is called in \LaravelJsonApi\Core\Resources\JsonApiResource::selfLink().

As per the JSON API spec the resource links are optional:

https://jsonapi.org/format/#document-resource-object-links

The optional links member within each resource object contains links related to the resource.
If present, this links object MAY contain a self link that identifies the resource represented by the resource object.
A server MUST respond to a GET request to the specified URL with a response that includes the resource as the primary data.

Default eager load paths

In the sections of the docs about the alwaysShowData method on the JSON:API relationship class, we mention that when using this you'd need to set default eager load paths on your schema to avoid "N+1" problems.

However, there is nothing in the eager loading chapter about default eager load paths.

Either this is an omission in the docs, or we haven't yet added it to the Eloquent implementation. Need to check, implement and update the docs.

Relationship URI should be moved to schema relation field

At the moment, if you want to use a different convention for a relationship's URI, you have to define it in two places.

Firstly, when registering the route:

$relationships->hasOne('blogPost')->uri('blog_post');

Secondly, on the resource's relation object:

$this->relation('blogAuthor')->retainFieldName()
// or
$this->relation('blogAuthor')->withUriFieldName('blog_author')

Now that we have made resource classes optional, we should move this setting to the schema's relation field. That would mean we could use it both when serializing the relation and when registering routes. (To use it when serializing relations we would need to always inject the schema into the resource class - even if using a specific resource class e.g. PostResource.)

415 for DELETE request with no body.

Your documentation states:

Any request that involves the client sending a JSON:API document in the request is processed by the resource request class. For example, our posts resource will have a PostRequest class.

A client specifies the media type of request content in the Content-Type header. If the client specifies a media-type other than the JSON:API media-type (application/vnd.api+json), they will receive a 415 Unsupported Media Type response.

And from what I understand in the JSON:API standard documentation, the client shouldn't set any Content-Type header if the request does not contain any body (which makes sense).

But if I send a DELETE request, for instance DELETE /api/v1/clients/311 I receive a 415 HTTP error code because the isJsonApifunction of the Http\Requests\FormRequest class in called.

Is it the expected behavior ?

Update to-many relation bug

Trying to update relations of workers resource

PATCH /api/v1/workers/1
...
'type' => 'workers',
'id' => "1",
'attributes' => [...],
'relationships' => [
    'skills' => [
        'data' => [
            'type' => 'skills',
            'id' => '4',
        ]
    ]
]
...

And getting the response

"errors": [
      {
          "detail": "The field skills must be a to-many relation.",
          "source": {
              "pointer": "/data/relationships/skills"
          },
          "status": "400",
          "title": "Non-Compliant JSON:API Document"
      }
  ],

Workers schema

return [
     ...
      BelongsToMany::make('skills'),
      ...
  ];

Workers resource

public function relationships($request): iterable
    {
        return [
            ...
            $this->relation('skills'),
            ...
        ];
    }

Workers request

public function rules(): array
    {
        return [
            'skills' => JsonApiRule::toMany(),
            ...
        ];
    }

Workers model

...
public function skills()
    {
        return $this->belongsToMany(Skill::class)->withTimestamps();
    }
...

Invalid relation type that does not exist rejects with incorrect message from spec validation

In a test I sent a to-one relation with the type member set to dance-events, when the relationship expects a products resource. As the id sent with the incorrect type did not exist, I received this error message:

{
    "errors": [
        {
            "detail": "The related resource does not exist.",
            "source": {
                "pointer": "/data/relationships/product"
            },
            "status": "404",
            "title": "Not Found"
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

What I should have actually received was a message saying the the dance-events resource was an invalid resource type for the relationship. The above message implies that the related resource is being tested for existence before being checked for it being the expected type. It would make more sense to check the type first, then check for existence.

Soft deletes are not yet supported

My model implements soft deleting just as laravel documentation is describing it: uses SoftDeletes trait and has deleted_at column in migration. When I call delete method of the model outside the json api server it works fine - I still have record with deleted_at column updated. But when I am deleting record using json api server route, it's just gone and acting like force delete. Where is the point I am losing here, how to implement soft delete inside json api server?

Inability to use dependency injection in the server class

The server class is currently constructed in \LaravelJsonApi\Core\Server\ServerRepository like this:

$server = new $class($this->container, $name);

I like to always use DI instead of facades and global helpers but that is not possible at the moment because of this line (for example when adding a scope that needs some dependency in the serving method of the server).
My suggestion would be to have the server bound in the container under a convention key, like jsonapi_server_{$name}.

So the line from above would become

$server = $this->container->make("jsonapi_server_{$name}");

That way we could completely override the binding in our code (or just contextually bind a dependency, like a guard for example if needed).

What do you think?

Generator for a generic authorizer expects a server to be specified

When running this:

php artisan jsonapi:authorizer organiser

You get the error message:

You must use the server option when you have more than one API.

However this is generating a generic authorizer, which goes in the App\JsonApi\Authorizers namespace - i.e. does not relate to a specific server. In this scenario it should not need the server to be specified.

Can't associate model to one which is creating

Using approach to test my create resource with relation route like here https://laraveljsonapi.io/docs/1.0/testing/resources.html#store-aka-create-testing
But unfortunately model wasn't associated, no error, just null instead of relation? Can you check this, can't figure it out, is it my mistake somewhere or it's a bug.

Worker model
public function schedules()
{
    return $this->hasMany(WorkerSchedule::class);
}
Worker Schedule model
public function worker()
{
    return $this->belongsTo(Worker::class);
}
Worker Schedules schema
public function fields(): array
{
...
    BelongsTo::make('worker'),
...
}
Worker Schedules resource
public function relationships($request): iterable
{
    return [
        $this->relation('worker')
    ];
}
Request inside the test
$response = $this
            ->jsonApi()
            ->expects('worker-schedules')
            ->withData([
                'type' => 'worker-schedules',
                'attributes' => $data,
                'relationships' => [
                    'worker' => [
                        'data' => [
                            'type' => 'workers',
                            'id' => (string) $worker->id,
                        ]
                    ],
                ],
            ])
            ->post('/api/v1/worker-schedules');
Result
Failed asserting that a row in the table [worker_schedules] matches the attributes {
    "id": "84",
    "worker_id": "77",
...
}.

Found similar results: [
    {
        "id": 84,
        "worker_id": null,
...
    }
].

Support sort parameters that do not relate to a column

At the moment, the package only supports sort parameters that directly relate to a field in the schema (i.e. a database column) - via the sortable() method.

It should also be possible to add sort parameters that don't relate to a field. Need to add this in before 1.0, as it is supported in the cloudcreativity/laravel-json-api package.

As per the sortByLikes example here:
https://laravel-json-api.readthedocs.io/en/latest/fetching/sorting/#implementing-sorting

And also it should be possible to do things like order-by sub-queries:
https://laravel-news.com/eloquent-subquery-enhancements

Discord Community?

I'm very much aware that asking lots of questions inside of GitHub issues is likely to cause a bit of a mess for the maintainers. Would you be open to having a Discord community for general questions and support?

I say Discord because it can be used for free and retains the message history, as opposed to Slack.

Ability to have a more complex logic for determining a singular filter

Currently the "singular filter" feature works on a per filter basis. In the old library the isSearchOne function existed on the adapter in which a more complex logic could be written, for example in the case of a composite primary key.

    protected function isSearchOne(Collection $filters): bool
    {
        if ($filters->has('userId') && $filters->has('clientId')) {
            return true;
        }

        return false;
    }

I'm not aware if/how the same thing can be achieved with this package.

Inconsistent method names for setting relation inverse type

At the moment, you call $relation->inverseType('type') to set the inverse type for a non-polymorph relation. But for a polymorph, you call $relation->types('type1', 'type2') to set the inverse type. This doesn't feel intuitive or consistent.

The types method should just be the plural of the method that's used to set a single type. Or if it's possible just use a single method for both, e.g. inverse. (But need to check what method names are being used by the implementation to get the inverse type.)

Sending sparse fields sets for a non-existent resource type does not give clear error message

By accident I sent sparse fields for the post resource, instead of posts. This resulted in the following error:

{
    "errors": [
        {
            "detail": "Sparse field sets post.slug, post.synopsis, post.title are not allowed.",
            "source": {
                "parameter": "fields"
            },
            "status": "400",
            "title": "Invalid Query Parameter"
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

The message should probably actually say that the post resource type does not exist.

Delete route always requires a resource request class when it should be optional

At the moment the delete route always expects there to be a resource request class, e.g. PostRequest. This is so that it can validate the delete request. However, implementing delete validation logic is entirely optional. Therefore, the delete route should work even when there isn't a resource request. It should just assume the deletion is allowed as the route has been registered and authorized by the time the deletion validation is attempted.

Controller hooks for modifying relationships should receive the relation value

As per this issue:
cloudcreativity/laravel-json-api#264

The controller hooks for modifying a relation should receive the relation value. We can use the LazyRelation value here, just like the authorizers.

The LazyRelation class actually caches its value internally for a to-many relation. We should add caching of the to-one relation as well. Then we should keep the instance on the request, so that the same instance is used for both the authorization and the controller hooks: meaning there would only be one database request if the relation is accessed in both the authorization code and the controller hook.

Filtering by many to many relation throws an exception

Trying to implement filtering resource by its relationship column, but it throws an exception Expecting pivot filter to be used with a belongs-to-many relation. {"userId":1,"exception":"[object] (LogicException(code: 0): Expecting pivot filter to be used with a belongs-to-many relation. at /var/www/html/vendor/laravel-json-api/eloquent/src/Filters/WherePivotIn.php:40). Have two resources: workers and dayoffs, many to many relation between them, defining filtering like here https://laraveljsonapi.io/docs/1.0/schemas/filters.html#wherepivotin.

DayOffs schema
public function fields(): array
    {
        return [
            ID::make(),

            DateTime::make('day'),
            BelongsToMany::make('workers'),

            DateTime::make('createdAt')->sortable()->readOnly(),
            DateTime::make('updatedAt')->sortable()->readOnly(),
        ];
    }

    public function filters(): array
    {
        return [
            WhereIn::make('id', $this->idColumn()),
            WherePivotIn::make('workers', 'worker_id')
        ];
    }

Did I miss something?

Lumen support

Hi!

First of all, great package. It does everything I need it to do and more. At my work we have a bunch of microservices written in Lumen, and we'e looking for a good solution to implement the json:api schema in these services. I read the doc, but couldn't find a mention of the Lumen framework anywhere. Is support for Lumen on the roadmap? And is it something that you might be accepting PR's for?

Thanks.

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.