GithubHelp home page GithubHelp logo

Comments (15)

lindyhopchris avatar lindyhopchris commented on August 16, 2024

Do you have a ContainerCollectionQuery class? If so, what's in that?

from laravel.

IAndreaGiuseppe avatar IAndreaGiuseppe commented on August 16, 2024

Thanks @lindyhopchris for the fast reply,

I dont use any **CollectionQuery at all, the only thing related may be the relationship method on the Container model

class Container extends Model
{
    // ...

    /**
     * A container may have many rows
     * 
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function rows()
    {
        return $this->hasMany(Row::class, 'container_id', 'id')
            ->select('rows.*')
            ->join('products', 'rows.product_id', '=', 'products.id')
            ->orderBy('products.is_visible', 'desc')
            ->orderBy('products.is_snoozed')
            ->orderBy('weight');
    }
}

but If I remove the custom sorting the results is the same without any "meta".

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

Does it work if you remove the select() from that?

Basically that isn't a typical Eloquent relationship there - so I'd suspect something to do with that is a problem.

Does it work if you just change it to:

    public function rows()
    {
        return $this->hasMany(Row::class, 'container_id', 'id');
    }

Which is a more typical Eloquent relationship? FYI I wouldn't recommend doing all those things on the relationship in the rows() method - it should be up to calling code that is querying the relationship as to which columns it wants to select, whether it needs to do a join, what order it wants things in, etc.

from laravel.

IAndreaGiuseppe avatar IAndreaGiuseppe commented on August 16, 2024

If I use this typical relation:

    /**
     * A container may have many rows
     * 
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function rows()
    {
        return $this->hasMany(Row::class, 'container_id', 'id');
    }

I still dont get any "meta" field on the response:

https://domain.com/v1/containers/115?withCount=rows

this is the full response btw

{
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "https://domain.com/v1/containers/115"
  },
  "data": {
    "type": "containers",
    "id": "115",
    "attributes": {
      "label": "prova",
      "weight": 3,
      "livemode": false,
      "container_id": null,
      "image_path": "assets/containers/115/y5f3IBxosTZbzCPuUQo7CP9r8UDR1NKlswOQKqNb.jpg",
      "image_url": "https://domain.s3.eu-central-1.amazonaws.com/assets/containers/115/y5f3IBxosTZbzCPuUQo7CP9r8UDR1NKlswOQKqNb.jpg?...blablabla",
      "description": null,
      "attributes": null,
      "created_at": "2024-01-22T15:57:01.000000Z",
      "updated_at": "2024-02-11T17:45:51.000000Z"
    },
    "relationships": {
      "rows": {
        "links": {
          "related": "https://domain.com/v1/containers/115/rows"
        }
      },
      "products": {
        "links": {
          "related": "https://domain.com/v1/containers/115/products"
        }
      },
      "containers": {
        "links": {
          "related": "https://domain.com/v1/containers/115/containers"
        }
      }
    },
    "links": {
      "self": "https://domain.com/v1/containers/115"
    }
  }
}

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

So this is working in the automated tests, so I'm going to need you to debug as it's impossible for me to say based on this information. Here's an automated test for it which is why I know it works:

public function testWithCount(): void
{
$post = Post::factory()
->has(Tag::factory()->count(1))
->has(Comment::factory()->count(3))
->create();
$expected = $this->serializer
->post($post)
->withRelationshipMeta('tags', ['count' => 1])
->withRelationshipMeta('comments', ['count' => 3]);
$response = $this
->withoutExceptionHandling()
->jsonApi('posts')
->query(['withCount' => 'comments,tags'])
->get(url('/api/v1/posts', $expected['id']));
$response->assertFetchedOneExact($expected);
}

Some things I'd need to know:

Do you have a custom controller action?
Do you have a ContainerQuery class? (got that wrong earlier when I asked about the ContainerCollectionQuery class.)

If you don't have a custom controller action, can you debug the model here:

To see whether it has the count information in it? I believe Eloquent would call it rows_count or something like that.

from laravel.

IAndreaGiuseppe avatar IAndreaGiuseppe commented on August 16, 2024

I was looking at the custom controller, I have a custom controller for Containers:

<?php

namespace App\Http\Controllers\Api\V1;

use Illuminate\Http\Request;
use LaravelJsonApi\Core\Document\Error;
use LaravelJsonApi\Laravel\Http\Controllers\Actions;

use App\Http\Controllers\Controller;
use App\Models\Business\Container;

class ContainersController extends Controller
{
    use Actions\FetchMany;
    use Actions\FetchOne;
    use Actions\Store;
    use Actions\Update;
    use Actions\FetchRelated;
    use Actions\FetchRelationship;
    use Actions\UpdateRelationship;
    use Actions\AttachRelationship;
    use Actions\DetachRelationship;

    /**
     * Delete a container and everything contained in it, including sub-containers.
     * 
     * @return \Illuminate\Http\Response
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function deleteAll(Request $request, Container $container)
    {
        $container->deleteAll();

        return response()->json([], 204);
    }

    /**
     * Delete only the container.
     * 
     * @return \Illuminate\Http\Response
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function deleteContainerOnly(Request $request, Container $container)
    {
        // Can't delete container if sub-containers are present
        if ($container->containers()->exists()) {
            return Error::make()->setStatus(400)->setDetail('Resource could not be deleted.');
        }

        $container->delete();

        return response()->json([], 204);
    }

    /**
     * Delete the container and all the products contained.
     * 
     * @return \Illuminate\Http\Response
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function deleteContainer(Request $request, Container $container)
    {
        // Can't delete container if sub-containers are present
        if ($container->containers()->exists()) {
            return Error::make()->setStatus(400)->setDetail('Resource could not be deleted.');
        }

        $container->deleteAll();

        return response()->json([], 204);
    }
}

I checked with another resource that hasnot a custom controller but I still dont get the "meta"

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

So the FetchOne action will be handling the https://domain.com/v1/containers/115?withCount=rows request, as you're fetching a single resource with id 115.

It would help if you can debug the model returned here:

In its attributes, does it have a rows_count value?

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

Just to clarify something - I'm not saying there isn't a bug in the package. But when I have a passing automated test for exactly this scenario, and I don't have access to your application, then I need you to provide more debug insight, because otherwise I'm just totally guessing as to what the problem might be!

from laravel.

IAndreaGiuseppe avatar IAndreaGiuseppe commented on August 16, 2024

Ok, I got it, yes there is a rows_count attribute on the model

App\Models\Business\Container {#2928 ▼ // vendor/laravel-json-api/laravel/src/Http/Controllers/Actions/FetchOne.php:59
  #connection: "mysql"
  #table: "containers"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #escapeWhenCastingToString: false
  #attributes: array:12 [▼
    "id" => 115
    "price_list_id" => 4
    "label" => "prova"
    "weight" => 3
    "livemode" => 0
    "container_id" => null
    "image_path" => "assets/containers/115/y5f3IBxosTZbzCPuUQo7CP9r8UDR1NKlswOQKqNb.jpg"
    "description" => null
    "attributes" => null
    "created_at" => "2024-01-22 15:57:01"
    "updated_at" => "2024-02-11 17:45:51"
    "rows_count" => 3
  ]
  #original: array:12 [▶]
  #changes: []
  #casts: array:4 [▶]
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: []
  #visible: []
  #fillable: array:6 [▶]
  #guarded: array:1 [▶]
}

Maybe the problem is on the resource? I use **Resource.php on every model

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

Thanks so much for debugging this - it's great to confirm that the value is in the model as that means this package is loading the data properly.

My next questions was going to be: are you using a resource class? Which you've already answered!

If you're using your own resource class, then you'd need to assign the meta to the relationship yourself:
https://laraveljsonapi.io/docs/3.0/resources/relationships.html#meta

Out of interest, why are you using resource classes? Generally it's not required.

from laravel.

IAndreaGiuseppe avatar IAndreaGiuseppe commented on August 16, 2024

Well done! Im glad we found out what's the problem.

We use resources cause we need to control what will be exposed to an external client by the api.

Now I have a question,
by using resources it seems to me that some of the "countable relationship" modifiers/methods are useless because I have to manage the meta field by myself.

            $this->relation('rows')->withoutSelfLink()->withMeta(['rows_count' => $this->rows_count]),

Ex. This will always include the meta for the relation, how do I include the meta only when requested by the client?

Something like this but seems a bit "procedural"

    /**
     * Get the resource's relationships.
     *
     * @param Request|null $request
     * @return iterable
     */
    public function relationships($request): iterable
    {
        return [
            $this->relation('rows')->withoutSelfLink()->withMeta(function () use ($request) {
                if ($request->has('withCount')) {
                    $countables = explode(',', $request->query('withCount'));

                    if (in_array('rows', $countables)) {
                        return ['rows_count' => $this->rows_count];
                    }
                }
            }),
            $this->relation('products')->withoutSelfLink(),
            $this->relation('containers')->withoutSelfLink(),
        ];
    }

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

Yeah you have to manage the meta field by yourself as you have chosen to manage the resource by yourself. If you want the package to manage the meta field you should not use a resource class.

As you've implemented a specific resource class, you shouldn't do anything "generic" like you're proposing, because it would be inefficient. In your resource class, you know that the row count might be there, so all you need to do is:

$this->relation('rows')->withMeta(fn () => ['count' => $this->rows_count]);

You can obviously put logic in there if you only want the count to show if rows_count on the model is an integer. You really really do not need to be checking the withCount on the request, because the model won't have rows_count as an attribute if it wasn't requested.

As a side note, I really wouldn't recommend using withoutSelfLink(). The spec specifies the self link should be there.

I'm probably going to have to start deprecating and removing some of the usages of the resource class as it's enabling people to deviate from the spec. Which is bad, because the whole point of the package is to implement the spec.

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

You might find it's better not to bother with the resource class, and instead use this feature:
https://laraveljsonapi.io/docs/3.0/schemas/relationships.html#relationship-serialization

I think if you use that (i.e. remove the resource class and instead rely on serialisation customisation in the schema class) then the count meta should automatically be added. You'd need to check that though.

from laravel.

IAndreaGiuseppe avatar IAndreaGiuseppe commented on August 16, 2024
$this->relation('rows')->withoutSelfLink()->withMeta(fn () => is_numeric($this->rows_count) ? ['rows_count' => $this->rows_count] : null),

one line code

Thank you very much for the advice you are giving me. I will ask the developers to do an in-depth analysis in order to simplify the code.

For the sake of "withoutSelfLink", from a frontend point of view, it is useless. With the frontend we must have the resource available and avoid as many calls to the API as possible. I decided not to show it: 1. to simplify the response json, 2. not to give fe/nders a chance to cause confusion.

For the point about resources, I find it is cleaner to have the code well structured and divided rather than having to manage everything on the schema but this is a stylistic preference more than anything else. The important thing is to be consistent in our decisions.

Thanks for what you do and keep it up!

from laravel.

lindyhopchris avatar lindyhopchris commented on August 16, 2024

Sure no problem and thanks for the explanation.

I'll leave this ticket open as I need to add something to the docs on countable relationships, making it clear you have to add the meta manually if you have a resource class.

from laravel.

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.