feat: re-write get all routes to use pagination (#1424)

rewrite get_all routes to use a pagination pattern to allow for better implementations of search, filter, and sorting on the frontend or by any client without fetching all the data. Additionally we added a CI check for running the Nuxt built to confirm that no TS errors were present. Finally, I had to remove the header support for the Shopping lists as the browser caching based off last_updated header was not allowing it to read recent updates due to how we're handling the updated_at property in the database with nested fields. This will have to be looked at in the future to reimplement. I'm unsure how many other routes have a similar issue. 

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
Michael Genson 2022-06-25 14:39:38 -05:00 committed by GitHub
parent c158672d12
commit cb15db2d27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 683 additions and 197 deletions

View file

@ -1,4 +1,4 @@
import { ApiRequestInstance } from "~/types/api";
import { ApiRequestInstance, PaginationData } from "~/types/api";
export interface CrudAPIInterface {
requests: ApiRequestInstance;
@ -18,13 +18,13 @@ export abstract class BaseAPI {
}
}
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType=CreateType> extends BaseAPI implements CrudAPIInterface {
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType> extends BaseAPI implements CrudAPIInterface {
abstract baseRoute: string;
abstract itemRoute(itemId: string | number): string;
async getAll(start = 0, limit = 9999, params = {} as any) {
return await this.requests.get<ReadType[]>(this.baseRoute, {
params: { start, limit, ...params },
async getAll(page = 1, perPage = -1, params = {} as any) {
return await this.requests.get<PaginationData<ReadType>>(this.baseRoute, {
params: { page, perPage, ...params },
});
}

View file

@ -261,7 +261,7 @@ export default defineComponent({
async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll();
if (data) {
shoppingLists.value = data;
shoppingLists.value = data.items ?? [];
}
}

View file

@ -131,10 +131,10 @@ export default defineComponent({
}
async function refreshTokens() {
const { data } = await userApi.recipes.share.getAll(0, 999, { recipe_id: props.recipeId });
const { data } = await userApi.recipes.share.getAll(1, -1, { recipe_id: props.recipeId });
if (data) {
state.tokens = data;
state.tokens = data.items ?? [];
}
}

View file

@ -30,11 +30,15 @@ export function useStoreActions<T extends BoundT>(
const allItems = useAsync(async () => {
const { data } = await api.getAll();
if (allRef) {
allRef.value = data;
if (data && allRef) {
allRef.value = data.items;
}
return data ?? [];
if (data) {
return data.items ?? [];
} else {
return [];
}
}, useAsyncKey());
loading.value = false;
@ -45,8 +49,8 @@ export function useStoreActions<T extends BoundT>(
loading.value = true;
const { data } = await api.getAll();
if (data && allRef) {
allRef.value = data;
if (data && data.items && allRef) {
allRef.value = data.items;
}
loading.value = false;

View file

@ -21,7 +21,12 @@ export const useTools = function (eager = true) {
loading.value = true;
const units = useAsync(async () => {
const { data } = await api.tools.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@ -33,7 +38,7 @@ export const useTools = function (eager = true) {
const { data } = await api.tools.getAll();
if (data) {
tools.value = data;
tools.value = data.items;
}
loading.value = false;

View file

@ -18,8 +18,8 @@ function swap(t: Array<unknown>, i: number, j: number) {
export const useSorter = () => {
function sortAToZ(list: Array<Recipe>) {
list.sort((a, b) => {
const textA = a.name?.toUpperCase() ?? "";
const textB = b.name?.toUpperCase() ?? "";
const textA: string = a.name?.toUpperCase() ?? "";
const textB: string = b.name?.toUpperCase() ?? "";
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
}
@ -61,10 +61,10 @@ export const useLazyRecipes = function () {
const recipes = ref<Recipe[]>([]);
async function fetchMore(start: number, limit: number, orderBy: string | null = null, orderDescending = true) {
const { data } = await api.recipes.getAll(start, limit, { orderBy, orderDescending });
async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") {
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
if (data) {
data.forEach((recipe) => {
data.items.forEach((recipe) => {
recipes.value?.push(recipe);
});
}
@ -80,26 +80,26 @@ export const useRecipes = (all = false, fetchRecipes = true) => {
const api = useUserApi();
// recipes is non-reactive!!
const { recipes, start, end } = (() => {
const { recipes, page, perPage } = (() => {
if (all) {
return {
recipes: allRecipes,
start: 0,
end: 9999,
page: 1,
perPage: -1,
};
} else {
return {
recipes: recentRecipes,
start: 0,
end: 30,
page: 1,
perPage: 30,
};
}
})();
async function refreshRecipes() {
const { data } = await api.recipes.getAll(start, end, { loadFood: true, orderBy: "created_at" });
const { data } = await api.recipes.getAll(page, perPage, { loadFood: true, orderBy: "created_at" });
if (data) {
recipes.value = data;
recipes.value = data.items;
}
}

View file

@ -31,7 +31,11 @@ export const useCookbooks = function () {
const units = useAsync(async () => {
const { data } = await api.cookbooks.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@ -41,8 +45,8 @@ export const useCookbooks = function () {
loading.value = true;
const { data } = await api.cookbooks.getAll();
if (data && cookbookStore) {
cookbookStore.value = data;
if (data && data.items && cookbookStore) {
cookbookStore.value = data.items;
}
loading.value = false;

View file

@ -30,9 +30,13 @@ export const useMealplans = function (range: Ref<DateRange>) {
limit: format(range.value.end, "yyyy-MM-dd"),
};
// @ts-ignore TODO Modify typing to allow for string start+limit for mealplans
const { data } = await api.mealplans.getAll(query.start, query.limit);
const { data } = await api.mealplans.getAll(1, -1, { start: query.start, limit: query.limit });
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@ -45,10 +49,10 @@ export const useMealplans = function (range: Ref<DateRange>) {
limit: format(range.value.end, "yyyy-MM-dd"),
};
// @ts-ignore TODO Modify typing to allow for string start+limit for mealplans
const { data } = await api.mealplans.getAll(query.start, query.limit);
const { data } = await api.mealplans.getAll(1, -1, { start: query.start, limit: query.limit });
if (data) {
mealplans.value = data;
if (data && data.items) {
mealplans.value = data.items;
}
loading.value = false;

View file

@ -14,7 +14,11 @@ export const useGroupWebhooks = function () {
const units = useAsync(async () => {
const { data } = await api.groupWebhooks.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@ -24,8 +28,8 @@ export const useGroupWebhooks = function () {
loading.value = true;
const { data } = await api.groupWebhooks.getAll();
if (data) {
webhooks.value = data;
if (data && data.items) {
webhooks.value = data.items;
}
loading.value = false;

View file

@ -43,7 +43,12 @@ export const useGroups = function () {
const asyncKey = String(Date.now());
const groups = useAsync(async () => {
const { data } = await api.groups.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, asyncKey);
loading.value = false;
@ -53,7 +58,13 @@ export const useGroups = function () {
async function refreshAllGroups() {
loading.value = true;
const { data } = await api.groups.getAll();
groups.value = data;
if (data) {
groups.value = data.items;
} else {
groups.value = null;
}
loading.value = false;
}

View file

@ -17,7 +17,11 @@ export const useAllUsers = function () {
const asyncKey = String(Date.now());
const allUsers = useAsync(async () => {
const { data } = await api.users.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, asyncKey);
loading.value = false;
@ -27,7 +31,13 @@ export const useAllUsers = function () {
async function refreshAllUsers() {
loading.value = true;
const { data } = await api.users.getAll();
users.value = data;
if (data) {
users.value = data.items;
} else {
users.value = null;
}
loading.value = false;
}

View file

@ -126,7 +126,7 @@ export default defineComponent({
const { data } = await api.mealplanRules.getAll();
if (data) {
allRules.value = data;
allRules.value = data.items ?? [];
}
}

View file

@ -22,21 +22,18 @@ import { useLazyRecipes } from "~/composables/recipes";
export default defineComponent({
components: { RecipeCardSection },
setup() {
// paging and sorting params
const page = ref(1);
const perPage = ref(30);
const orderBy = "name";
const orderDescending = false;
const increment = ref(30);
const orderDirection = "asc";
const start = ref(0);
const offset = ref(increment.value);
const limit = ref(increment.value);
const ready = ref(false);
const loading = ref(false);
const { recipes, fetchMore } = useLazyRecipes();
onMounted(async () => {
await fetchMore(start.value, limit.value, orderBy, orderDescending);
await fetchMore(page.value, perPage.value, orderBy, orderDirection);
ready.value = true;
});
@ -45,9 +42,8 @@ export default defineComponent({
return;
}
loading.value = true;
start.value = offset.value + 1;
offset.value = offset.value + increment.value;
fetchMore(start.value, limit.value, orderBy, orderDescending);
page.value = page.value + 1;
fetchMore(page.value, perPage.value, orderBy, orderDirection);
loading.value = false;
}, 500);

View file

@ -193,11 +193,11 @@ import { useCopyList } from "~/composables/use-copy";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
import { MultiPurposeLabelOut } from "~/types/api-types/labels";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/types/api-types/group";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { getDisplayText } from "~/composables/use-display-text";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
type CopyTypes = "plain" | "markdown";
@ -336,17 +336,9 @@ export default defineComponent({
// Labels, Units, Foods
// TODO: Extract to Composable
const allLabels = ref([] as MultiPurposeLabelOut[]);
const allUnits = useAsync(async () => {
const { data } = await userApi.units.getAll();
return data ?? [];
}, useAsyncKey());
const allFoods = useAsync(async () => {
const { data } = await userApi.foods.getAll();
return data ?? [];
}, useAsyncKey());
const { labels: allLabels } = useLabelStore();
const { units: allUnits } = useUnitStore();
const { foods: allFoods } = useFoodStore();
function sortByLabels() {
byLabel.value = !byLabel.value;
@ -405,7 +397,10 @@ export default defineComponent({
async function refreshLabels() {
const { data } = await userApi.multiPurposeLabels.getAll();
allLabels.value = data ?? [];
if (data) {
allLabels.value = data.items ?? [];
}
}
refreshLabels();

View file

@ -60,7 +60,12 @@ export default defineComponent({
async function fetchShoppingLists() {
const { data } = await userApi.shopping.lists.getAll();
return data;
if (!data) {
return [];
}
return data.items;
}
async function refresh() {

View file

@ -13,3 +13,11 @@ export interface ApiRequestInstance {
patch<T, U = Partial<T>>(url: string, data: U): Promise<RequestResponse<T>>;
delete<T>(url: string): Promise<RequestResponse<T>>;
}
export interface PaginationData<T> {
page: number;
per_page: number;
total: number;
total_pages: number;
items: T[];
}