mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-02 07:01:23 -05: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:
8
mealie/routes/organizers/__init__.py
Normal file
8
mealie/routes/organizers/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import controller_categories, controller_tags, controller_tools
|
||||
|
||||
router = APIRouter(prefix="/organizers")
|
||||
router.include_router(controller_categories.router)
|
||||
router.include_router(controller_tags.router)
|
||||
router.include_router(controller_tools.router)
|
||||
87
mealie/routes/organizers/controller_categories.py
Normal file
87
mealie/routes/organizers/controller_categories.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import UUID4, BaseModel
|
||||
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
||||
from mealie.schema.recipe.recipe import RecipeCategory
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave
|
||||
|
||||
router = APIRouter(prefix="/categories", tags=["Organizer: Categories"])
|
||||
|
||||
|
||||
class CategorySummary(BaseModel):
|
||||
id: UUID4
|
||||
slug: str
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
@controller(router)
|
||||
class RecipeCategoryController(BaseUserController):
|
||||
# =========================================================================
|
||||
# CRUD Operations
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.categories.by_group(self.group_id)
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins(self.repo, self.deps.logger)
|
||||
|
||||
@router.get("", response_model=list[CategorySummary])
|
||||
def get_all(self):
|
||||
"""Returns a list of available categories in the database"""
|
||||
return self.repo.get_all(override_schema=CategorySummary)
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_one(self, category: CategoryIn):
|
||||
"""Creates a Category in the database"""
|
||||
save_data = mapper.cast(category, CategorySave, group_id=self.group_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=CategorySummary)
|
||||
def get_one(self, item_id: UUID4):
|
||||
"""Returns a list of recipes associated with the provided category."""
|
||||
category_obj = self.mixins.get_one(item_id)
|
||||
category_obj = CategorySummary.from_orm(category_obj)
|
||||
return category_obj
|
||||
|
||||
@router.put("/{item_id}", response_model=CategorySummary)
|
||||
def update_one(self, item_id: UUID4, update_data: CategoryIn):
|
||||
"""Updates an existing Tag in the database"""
|
||||
save_data = mapper.cast(update_data, CategorySave, group_id=self.group_id)
|
||||
return self.mixins.update_one(save_data, item_id)
|
||||
|
||||
@router.delete("/{item_id}")
|
||||
def delete_one(self, item_id: UUID4):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
self.mixins.delete_one(item_id)
|
||||
|
||||
# =========================================================================
|
||||
# Read All Operations
|
||||
|
||||
@router.get("/empty", response_model=list[CategoryBase])
|
||||
def get_all_empty(self):
|
||||
"""Returns a list of categories that do not contain any recipes"""
|
||||
return self.repos.categories.get_empty()
|
||||
|
||||
@router.get("/slug/{category_slug}")
|
||||
def get_one_by_slug(self, category_slug: str):
|
||||
"""Returns a category object with the associated recieps relating to the category"""
|
||||
category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
|
||||
return RecipeCategoryResponse.construct(
|
||||
id=category.id,
|
||||
slug=category.slug,
|
||||
name=category.name,
|
||||
recipes=self.repos.recipes.by_group(self.group_id).get_by_categories([category]),
|
||||
)
|
||||
66
mealie/routes/organizers/controller_tags.py
Normal file
66
mealie/routes/organizers/controller_tags.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.recipe import RecipeTagResponse, TagIn
|
||||
from mealie.schema.recipe.recipe import RecipeTag
|
||||
from mealie.schema.recipe.recipe_category import TagSave
|
||||
|
||||
router = APIRouter(prefix="/tags", tags=["Organizer: Tags"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class TagController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.tags.by_group(self.group_id)
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins(self.repo, self.deps.logger)
|
||||
|
||||
@router.get("")
|
||||
async def get_all(self):
|
||||
"""Returns a list of available tags in the database"""
|
||||
return self.repo.get_all(override_schema=RecipeTag)
|
||||
|
||||
@router.get("/empty")
|
||||
def get_empty_tags(self):
|
||||
"""Returns a list of tags that do not contain any recipes"""
|
||||
return self.repo.get_empty()
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeTagResponse)
|
||||
def get_one(self, item_id: UUID4):
|
||||
"""Returns a list of recipes associated with the provided tag."""
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_one(self, tag: TagIn):
|
||||
"""Creates a Tag in the database"""
|
||||
save_data = mapper.cast(tag, TagSave, group_id=self.group_id)
|
||||
return self.repo.create(save_data)
|
||||
|
||||
@router.put("/{item_id}", response_model=RecipeTagResponse)
|
||||
def update_one(self, item_id: UUID4, new_tag: TagIn):
|
||||
"""Updates an existing Tag in the database"""
|
||||
save_data = mapper.cast(new_tag, TagSave, group_id=self.group_id)
|
||||
return self.repo.update(item_id, save_data)
|
||||
|
||||
@router.delete("/{item_id}")
|
||||
def delete_recipe_tag(self, item_id: UUID4):
|
||||
"""Removes a recipe tag from the database. Deleting a
|
||||
tag does not impact a recipe. The tag will be removed
|
||||
from any recipes that contain it"""
|
||||
|
||||
try:
|
||||
self.repo.delete(item_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST) from e
|
||||
|
||||
@router.get("/slug/{tag_slug}", response_model=RecipeTagResponse)
|
||||
async def get_one_by_slug(self, tag_slug: str):
|
||||
return self.repo.get_one(tag_slug, "slug", override_schema=RecipeTagResponse)
|
||||
50
mealie/routes/organizers/controller_tools.py
Normal file
50
mealie/routes/organizers/controller_tools.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe import RecipeTool
|
||||
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse, RecipeToolSave
|
||||
|
||||
router = APIRouter(prefix="/tools", tags=["Organizer: Tools"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class RecipeToolController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.tools.by_group(self.group_id)
|
||||
|
||||
@property
|
||||
def mixins(self) -> CrudMixins:
|
||||
return CrudMixins[RecipeToolCreate, RecipeTool, RecipeToolCreate](self.repo, self.deps.logger)
|
||||
|
||||
@router.get("", response_model=list[RecipeTool])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=RecipeTool)
|
||||
|
||||
@router.post("", response_model=RecipeTool, status_code=201)
|
||||
def create_one(self, data: RecipeToolCreate):
|
||||
save_data = mapper.cast(data, RecipeToolSave, group_id=self.group_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeTool)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=RecipeTool)
|
||||
def update_one(self, item_id: UUID4, data: RecipeToolCreate):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=RecipeTool)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
return self.mixins.delete_one(item_id) # type: ignore
|
||||
|
||||
@router.get("/slug/{tool_slug}", response_model=RecipeToolResponse)
|
||||
async def get_one_by_slug(self, tool_slug: str):
|
||||
return self.repo.get_one(tool_slug, "slug", override_schema=RecipeToolResponse)
|
||||
Reference in New Issue
Block a user