mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-11 07:37:14 -07:00
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:
parent
c158672d12
commit
cb15db2d27
55 changed files with 683 additions and 197 deletions
|
@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ export default defineComponent({
|
|||
const { data } = await api.mealplanRules.getAll();
|
||||
|
||||
if (data) {
|
||||
allRules.value = data;
|
||||
allRules.value = data.items ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue