mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-24 15:49:42 +02:00
feature: proper multi-tenant-support (#969)(WIP)
* update naming * refactor tests to use shared structure * shorten names * add tools test case * refactor to support multi-tenant * set group_id on creation * initial refactor for multitenant tags/cats * spelling * additional test case for same valued resources * fix recipe update tests * apply indexes to foreign keys * fix performance regressions * handle unknown exception * utility decorator for function debugging * migrate recipe_id to UUID * GUID for recipes * remove unused import * move image functions into package * move utilities to packages dir * update import * linter * image image and asset routes * update assets and images to use UUIDs * fix migration base * image asset test coverage * use ids for categories and tag crud functions * refactor recipe organizer test suite to reduce duplication * add uuid serlization utility * organizer base router * slug routes testing and fixes * fix postgres error * adopt UUIDs * move tags, categories, and tools under "organizers" umbrella * update composite label * generate ts types * fix import error * update frontend types * fix type errors * fix postgres errors * fix #978 * add null check for title validation * add note in docs on multi-tenancy
This commit is contained in:
parent
9a82a172cb
commit
c617251f4c
157 changed files with 1866 additions and 1578 deletions
|
@ -1,47 +0,0 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
export interface Category {
|
||||
name: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
recipes?: Recipe[];
|
||||
}
|
||||
|
||||
export interface CreateCategory {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const routes = {
|
||||
categories: `${prefix}/categories`,
|
||||
categoriesEmpty: `${prefix}/categories/empty`,
|
||||
|
||||
categoriesCategory: (category: string) => `${prefix}/categories/${category}`,
|
||||
};
|
||||
|
||||
export class CategoriesAPI extends BaseCRUDAPI<Category, CreateCategory> {
|
||||
baseRoute: string = routes.categories;
|
||||
itemRoute = routes.categoriesCategory;
|
||||
|
||||
/** Returns a list of categories that do not contain any recipes
|
||||
*/
|
||||
async getEmptyCategories() {
|
||||
return await this.requests.get(routes.categoriesEmpty);
|
||||
}
|
||||
|
||||
/** Returns a list of recipes associated with the provided category.
|
||||
*/
|
||||
async getAllRecipesByCategory(category: string) {
|
||||
return await this.requests.get(routes.categoriesCategory(category));
|
||||
}
|
||||
|
||||
/** Removes a recipe category from the database. Deleting a
|
||||
* category does not impact a recipe. The category will be removed
|
||||
* from any recipes that contain it
|
||||
*/
|
||||
async deleteRecipeCategory(category: string) {
|
||||
return await this.requests.delete(routes.categoriesCategory(category));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { Category } from "./categories";
|
||||
import { CategoryBase } from "~/types/api-types/recipe";
|
||||
import { RecipeCategory } from "~/types/api-types/user";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
|
@ -14,7 +14,7 @@ export interface CookBook extends CreateCookBook {
|
|||
description: string;
|
||||
position: number;
|
||||
group_id: number;
|
||||
categories: Category[] | CategoryBase[];
|
||||
categories: RecipeCategory[] | CategoryBase[];
|
||||
}
|
||||
|
||||
const routes = {
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface CreateMealPlan {
|
|||
entryType: PlanEntryType;
|
||||
title: string;
|
||||
text: string;
|
||||
recipeId?: number;
|
||||
recipeId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMealPlan extends CreateMealPlan {
|
||||
|
|
|
@ -12,7 +12,7 @@ const prefix = "/api";
|
|||
const routes = {
|
||||
shoppingLists: `${prefix}/groups/shopping/lists`,
|
||||
shoppingListsId: (id: string) => `${prefix}/groups/shopping/lists/${id}`,
|
||||
shoppingListIdAddRecipe: (id: string, recipeId: number) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
|
||||
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
|
||||
|
||||
shoppingListItems: `${prefix}/groups/shopping/items`,
|
||||
shoppingListItemsId: (id: string) => `${prefix}/groups/shopping/items/${id}`,
|
||||
|
@ -22,11 +22,11 @@ export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListOut, ShoppingListC
|
|||
baseRoute = routes.shoppingLists;
|
||||
itemRoute = routes.shoppingListsId;
|
||||
|
||||
async addRecipe(itemId: string, recipeId: number) {
|
||||
async addRecipe(itemId: string, recipeId: string) {
|
||||
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {});
|
||||
}
|
||||
|
||||
async removeRecipe(itemId: string, recipeId: number) {
|
||||
async removeRecipe(itemId: string, recipeId: string) {
|
||||
return await this.requests.delete(routes.shoppingListIdAddRecipe(itemId, recipeId));
|
||||
}
|
||||
}
|
||||
|
|
20
frontend/api/class-interfaces/organizer-categories.ts
Normal file
20
frontend/api/class-interfaces/organizer-categories.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { CategoryIn, RecipeCategoryResponse } from "~/types/api-types/recipe";
|
||||
import { config } from "~/api/config";
|
||||
|
||||
const prefix = config.PREFIX + "/organizers";
|
||||
|
||||
const routes = {
|
||||
categories: `${prefix}/categories`,
|
||||
categoriesId: (category: string) => `${prefix}/categories/${category}`,
|
||||
categoriesSlug: (category: string) => `${prefix}/categories/slug/${category}`,
|
||||
};
|
||||
|
||||
export class CategoriesAPI extends BaseCRUDAPI<RecipeCategoryResponse, CategoryIn> {
|
||||
baseRoute: string = routes.categories;
|
||||
itemRoute = routes.categoriesId;
|
||||
|
||||
async bySlug(slug: string) {
|
||||
return await this.requests.get<RecipeCategoryResponse>(routes.categoriesSlug(slug));
|
||||
}
|
||||
}
|
20
frontend/api/class-interfaces/organizer-tags.ts
Normal file
20
frontend/api/class-interfaces/organizer-tags.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { RecipeTagResponse, TagIn } from "~/types/api-types/recipe";
|
||||
import { config } from "~/api/config";
|
||||
|
||||
const prefix = config.PREFIX + "/organizers";
|
||||
|
||||
const routes = {
|
||||
tags: `${prefix}/tags`,
|
||||
tagsId: (tag: string) => `${prefix}/tags/${tag}`,
|
||||
tagsSlug: (tag: string) => `${prefix}/tags/slug/${tag}`,
|
||||
};
|
||||
|
||||
export class TagsAPI extends BaseCRUDAPI<RecipeTagResponse, TagIn> {
|
||||
baseRoute: string = routes.tags;
|
||||
itemRoute = routes.tagsId;
|
||||
|
||||
async bySlug(slug: string) {
|
||||
return await this.requests.get<RecipeTagResponse>(routes.tagsSlug(slug));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { RecipeTool, RecipeToolCreate, RecipeToolResponse } from "~/types/api-types/recipe";
|
||||
|
||||
const prefix = "/api";
|
||||
import { config } from "~/api/config";
|
||||
|
||||
const prefix = config.PREFIX + "/organizers";
|
||||
|
||||
const routes = {
|
||||
tools: `${prefix}/tools`,
|
||||
|
@ -13,7 +15,7 @@ export class ToolsApi extends BaseCRUDAPI<RecipeTool, RecipeToolCreate> {
|
|||
baseRoute: string = routes.tools;
|
||||
itemRoute = routes.toolsId;
|
||||
|
||||
async byslug(slug: string) {
|
||||
async bySlug(slug: string) {
|
||||
return await this.requests.get<RecipeToolResponse>(routes.toolsSlug(slug));
|
||||
}
|
||||
}
|
|
@ -1,22 +1,14 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { CreateIngredientFood, IngredientFood } from "~/types/api-types/recipe";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
export interface CreateFood {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Food extends CreateFood {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const routes = {
|
||||
food: `${prefix}/foods`,
|
||||
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
|
||||
};
|
||||
|
||||
export class FoodAPI extends BaseCRUDAPI<Food, CreateFood> {
|
||||
export class FoodAPI extends BaseCRUDAPI<IngredientFood, CreateIngredientFood> {
|
||||
baseRoute: string = routes.food;
|
||||
itemRoute = routes.foodsFood;
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ const routes = {
|
|||
};
|
||||
|
||||
export interface RecipeShareTokenCreate {
|
||||
recipeId: number;
|
||||
recipeId: string;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export interface RecipeShareToken {
|
||||
recipeId: number;
|
||||
recipeId: string;
|
||||
id: string;
|
||||
groupId: number;
|
||||
expiresAt: string;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Category } from "../categories";
|
||||
import { Tag } from "../tags";
|
||||
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
|
||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||
|
||||
export type Parser = "nlp" | "brute";
|
||||
|
||||
|
@ -30,8 +29,8 @@ export interface ParsedIngredient {
|
|||
|
||||
export interface BulkCreateRecipe {
|
||||
url: string;
|
||||
categories: Category[];
|
||||
tags: Tag[];
|
||||
categories: RecipeCategory[];
|
||||
tags: RecipeTag[];
|
||||
}
|
||||
|
||||
export interface BulkCreatePayload {
|
||||
|
@ -50,7 +49,7 @@ export interface CreateAsset {
|
|||
}
|
||||
|
||||
export interface RecipeCommentCreate {
|
||||
recipeId: number;
|
||||
recipeId: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { Recipe } from "~/types/api-types/admin";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
export interface Tag {
|
||||
name: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
recipes?: Recipe[];
|
||||
}
|
||||
|
||||
export interface CreateTag {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const routes = {
|
||||
tags: `${prefix}/tags`,
|
||||
tagsEmpty: `${prefix}/tags/empty`,
|
||||
|
||||
tagsTag: (tag: string) => `${prefix}/tags/${tag}`,
|
||||
};
|
||||
|
||||
export class TagsAPI extends BaseCRUDAPI<Tag, CreateTag> {
|
||||
baseRoute: string = routes.tags;
|
||||
itemRoute = routes.tagsTag;
|
||||
|
||||
/** Returns a list of categories that do not contain any recipes
|
||||
*/
|
||||
async getEmptyCategories() {
|
||||
return await this.requests.get(routes.tagsEmpty);
|
||||
}
|
||||
|
||||
/** Returns a list of recipes associated with the provided category.
|
||||
*/
|
||||
async getAllRecipesByCategory(category: string) {
|
||||
return await this.requests.get(routes.tagsTag(category));
|
||||
}
|
||||
|
||||
/** Removes a recipe category from the database. Deleting a
|
||||
* category does not impact a recipe. The category will be removed
|
||||
* from any recipes that contain it
|
||||
*/
|
||||
async deleteRecipeCategory(category: string) {
|
||||
return await this.requests.delete(routes.tagsTag(category));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue