1
0
Fork 0
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:
Hayden 2022-02-13 12:23:42 -09:00 committed by GitHub
parent 9a82a172cb
commit c617251f4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
157 changed files with 1866 additions and 1578 deletions

View file

@ -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));
}
}

View file

@ -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 = {

View file

@ -16,7 +16,7 @@ export interface CreateMealPlan {
entryType: PlanEntryType;
title: string;
text: string;
recipeId?: number;
recipeId?: string;
}
export interface UpdateMealPlan extends CreateMealPlan {

View file

@ -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));
}
}

View 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));
}
}

View 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));
}
}

View file

@ -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));
}
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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));
}
}