Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support different API response structures (for pagination) #21

Closed
charliegilmanuk opened this issue Feb 22, 2019 · 9 comments
Closed

Support different API response structures (for pagination) #21

charliegilmanuk opened this issue Feb 22, 2019 · 9 comments

Comments

@charliegilmanuk
Copy link

charliegilmanuk commented Feb 22, 2019

Frameworks like Laravel often have built in pagination methods, which in turn change the default return data from the API. plugin-axios current $fetch method requires that the model data is in the root of the response array, however Laravel's pagination method returns something like the following, with the data nested in the response:

{
  current_page: 1,
  data: [
    { ID: 512, FirstName: 'Bob', LastName: 'Smith' },
    { ID: 747, FirstName: 'Ryan', LastName: 'Mack' },
    ...
  ],
  first_page_url: "http://example.test/api/users?page=1",
  from: 1,
  last_page: 215,
  last_page_url: "http://example.test/api/users?page=215",
  next_page_url: "http://example.test/api/users?page=2",
  path: "http://example.test/api/users",
  per_page: 75,
  prev_page_url: null,
  to: 75,
  total: 16113
}

It is necessary that this data is paginated server side, as you can see there is a total of 16113 records, which in reality have many more columns and each have their own relationships.

Would it be possible to cater for this by including some configuration (either globally or within each model, maybe even just as simple as paginated: true would be amazing) where you could specify the expected response structure, or key of the data or something? It would be great if the pagination keys could be stored too, although would not need to be queried in the same way as the data.

At the moment I simply can't use this as half of my API routes return paginated data, which I'm sure is common. I've tested a route without it and it works perfectly so thank you for this, huge huge help with working with a large scale SPA.

Maybe even be as simple as changing this in Fetch.js from:

static onSuccess(commit, model, data) {
  commit('onSuccess')

  model.insertOrUpdate({
    data,
  });
}

To this:

static onSuccess(commit, model, data) {
  commit('onSuccess')

  if (model.paginated) {
    model.insertOrUpdate(data);
  } else {
    model.insertOrUpdate({
      data
    })
  }
}

But I'm unsure of what other side effects this may have or what exactly insertOrUpdate is expecting.

Many thanks in advance.

EDIT: Have since tried the insertOrUpdate part in the promise returned from $fetch, this got the data working great (albeit with an empty default record at index 0) but doesn't populate any of the pagination values, even after manually specifying the keys in each model's state.

@aveeday
Copy link

aveeday commented Mar 12, 2019

How about customize onResponse function in options:

VuexORM.use(VuexORMAxios, {
  http: {
    onResponse(response) {
      // For api with pagination
      if (response.data && response.data.results) {
        return response.data.results;
      }
      // For api without pagination
      return response.data;
    },
  },
});

In my case data containing in { results: [] }.
Also here you have access to response.data.total and other data in response.

@russsiq
Copy link

russsiq commented Mar 29, 2019

I define different status codes in the Laravel controller. This allows more correct processing of query results.

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class ArticleResource extends JsonResource
{
    /**
     * The "data" wrapper that should be applied.
     *
     * @var string
     */
    public static $wrap = null;

    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return parent::toArray($request);
    }
}
use Illuminate\Http\Response;

...

class ArticlesController extends Controller
{

    public function index()
    {
        $articles = Article::with([
                'categories:categories.id,categories.title,categories.slug',
                'user:users.id,users.name',
            ])->withCount([
                'comments',
                'files',
            ])->paginate();

        $collection = ArticleResource::collection($articles);

        return $collection->response()
            ->setStatusCode(Response::HTTP_PARTIAL_CONTENT);
    }

    public function store(ArticleRequest $request)
    {
        ...
            ->setStatusCode(Response::HTTP_CREATED);
    }

    public function show(int $id)
    {
        $article = Article::findOrFail($id);

        // 200 Response::HTTP_OK
        return new ArticleResource($article);
    }

    public function update(ArticleRequest $request, int $id)
    {
        ...
            ->setStatusCode(Response::HTTP_ACCEPTED);
    }
    public function destroy(int $id)
    {
        ...
        return response()->json(null, Response::HTTP_NO_CONTENT);
    }
// store/index.js
...
// Import Vuex plugins.
import VuexORM from '@vuex-orm/core'
import VuexORMAxios from '@vuex-orm/plugin-axios'
import database from './database'
import http from './axios-request-config'

VuexORM.use(VuexORMAxios, {
    http,
    database,
})
...
// store/axios-request-config.js

import store from '@/store'
import router from '@/router'

export default {
    axios: undefined,
...
    onResponse(response) {
        const statuses = {
            201: this.onCreatedResponse,
            202: this.onUpdatedResponse,
            204: this.onDeletedResponse,
            206: this.onPartialContent,
        }

        return response.status in statuses
            ? statuses[response.status](response)
            : response.data
    },
...
    /**
     * On 206 Partial Content
     * @param {object} response
     */
    onPartialContent(response) {
        const {
            data,
            meta
        } = response.data

        store.dispatch('paginator/paginate', meta)

        router.replace({
            query: {
                'page': meta.current_page
            }
        })

        return data
    },
...
}
// views\articles\index.vue
...
    methods: {
        async index(page = 1) {
            // Reset resource list.
            await this.model.deleteAll()

            // Fetch article resource.
            await this.model.$fetch({
                query: {
                    page: parseInt(page, 10)
                }
            })
        },
...

@malinbranduse
Copy link

@russsiq Awesome idea with the status codes.
Would you mind providing the store module for the pagination?

...
store.dispatch('paginator/paginate', meta) // this module/action
...

@russsiq
Copy link

russsiq commented Aug 17, 2019

@malinushj, you can look here https://gist.github.com/russsiq/a6279e15bb0edee63d1da0e71839cba3

@malinbranduse
Copy link

@russsiq Awesome! thank you!

kpabi pushed a commit to kpabi/plugin-axios that referenced this issue Sep 13, 2019
Useful for pagination.
Related to issue vuex-orm#21
@kiaking
Copy link
Member

kiaking commented Oct 21, 2019

The brand new Axios plugin is here! It's very different from what it was, so please check out the docs!

With the new version, there's config dataKey that you can specify what key the library should look for 👍

@kiaking kiaking closed this as completed Oct 21, 2019
@malinbranduse
Copy link

@kiaking bless

@allocenx
Copy link

@kiaking i still do not get it, how does this even help with the pagination? I want to store the full data and pagination aswell as i'm doing an infinite scrolling ...

@mblarsen
Copy link

This is not directly related to the issue, but I'm writing here in case it might help someone else.

When you do pagination (w/sort) you'll also need to know which elements belong on the "current page", but this plugin uses insertOrUpdate after fetching your "page" from the end-point, essentially adding the "first" and the "second" page when you navigate.

What you have to do is is replace instead of insert. The only way to do that AFIK is to deleteAll() model. Unfortunately doing so will remove the items right away while waiting for the end-point to return (if ever and successfully).

You can pass {save: false} as the config option to the request methods, which lets you inspect the result before persisting to the store. E.g.

const result = await Job.api().get('/api/jobs', {save: false})
Job.deleteAll() // delete first
result.save() // this commits to store

So by manually saving to store you don't get that bad UX where your list is empty while waiting for the next page.

Other methods I've explored is instead saving the IDs for the current page that way you don't have to deleteAll() first. But instead you use the ORM to like Job.query().whereIn(pageIds).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants