mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-13 19:33:12 -05:00
feat(backend): ✨ start multi-tenant support (WIP) (#680)
* fix ts types * feat(code-generation): ♻️ update code-generation formats * new scope * add step button * fix linter error * update code-generation tags * feat(backend): ✨ start multi-tenant support * feat(backend): ✨ group invitation token generation and signup * refactor(backend): ♻️ move group admin actions to admin router * set url base to include `/admin` * feat(frontend): ✨ generate user sign-up links * test(backend): ✅ refactor test-suite to further decouple tests (WIP) * feat(backend): 🐛 assign owner on backup import for recipes * fix(backend): 🐛 assign recipe owner on migration from other service Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
from fastapi import Depends, Response
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.dependencies import is_logged_in
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import SessionLocal, generate_session
|
||||
from mealie.schema.recipe import RecipeSummary
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class AllRecipesService:
|
||||
def __init__(self, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
|
||||
self.start = 0
|
||||
self.limit = 9999
|
||||
self.session = session or SessionLocal()
|
||||
self.is_user = is_user
|
||||
|
||||
@classmethod
|
||||
def query(
|
||||
cls, start=0, limit=9999, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
|
||||
):
|
||||
set_query = cls(session, is_user)
|
||||
|
||||
set_query.start = start
|
||||
set_query.limit = limit
|
||||
|
||||
return set_query
|
||||
|
||||
def get_recipes(self):
|
||||
if self.is_user:
|
||||
return get_all_recipes_user(self.limit, self.start)
|
||||
|
||||
else:
|
||||
return get_all_recipes_public(self.limit, self.start)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_all_recipes_user(limit, start):
|
||||
with SessionLocal() as session:
|
||||
all_recipes: list[RecipeSummary] = db.recipes.get_all(
|
||||
session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary
|
||||
)
|
||||
all_recipes_json = [recipe.dict() for recipe in all_recipes]
|
||||
return Response(content=json.dumps(jsonable_encoder(all_recipes_json)), media_type="application/json")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_all_recipes_public(limit, start):
|
||||
with SessionLocal() as session:
|
||||
all_recipes: list[RecipeSummary] = db.recipes.get_all_public(
|
||||
session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary
|
||||
)
|
||||
all_recipes_json = [recipe.dict() for recipe in all_recipes]
|
||||
return Response(content=json.dumps(jsonable_encoder(all_recipes_json)), media_type="application/json")
|
||||
|
||||
|
||||
def clear_all_cache():
|
||||
get_all_recipes_user.cache_clear()
|
||||
get_all_recipes_public.cache_clear()
|
||||
logger.info("All Recipes Cache Cleared")
|
||||
|
||||
|
||||
def subscripte_to_recipe_events():
|
||||
db.recipes.subscribe(clear_all_cache)
|
||||
logger.info("All Recipes Subscribed to Database Events")
|
||||
16
mealie/services/recipe/mixins.py
Normal file
16
mealie/services/recipe/mixins.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
def recipe_creation_factory(user: PrivateUser, name: str, additional_attrs: dict = None) -> Recipe:
|
||||
"""
|
||||
The main creation point for recipes. The factor method returns an instance of the
|
||||
Recipe Schema class with the appropriate defaults set. Recipes shoudld not be created
|
||||
else-where to avoid conflicts.
|
||||
"""
|
||||
additional_attrs = additional_attrs or {}
|
||||
additional_attrs["name"] = name
|
||||
additional_attrs["user_id"] = user.id
|
||||
additional_attrs["group_id"] = user.group_id
|
||||
|
||||
return Recipe(**additional_attrs)
|
||||
@@ -7,14 +7,15 @@ from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from mealie.core.dependencies.grouped import PublicDeps, UserDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
|
||||
from mealie.services._base_http_service.http_services import PublicHttpService
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
from mealie.services.recipe.mixins import recipe_creation_factory
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class RecipeService(PublicHttpService[str, Recipe]):
|
||||
class RecipeService(UserHttpService[str, Recipe]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
@@ -46,9 +47,13 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
return self.item
|
||||
|
||||
# CRUD METHODS
|
||||
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
if isinstance(create_data, CreateRecipe):
|
||||
create_data = Recipe(name=create_data.name)
|
||||
def get_all(self, start=0, limit=None):
|
||||
return self.db.recipes.multi_query(
|
||||
self.session, {"group_id": self.user.group_id}, start=start, limit=limit, override_schema=RecipeSummary
|
||||
)
|
||||
|
||||
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())
|
||||
|
||||
try:
|
||||
self.item = self.db.recipes.create(self.session, create_data)
|
||||
@@ -56,13 +61,13 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._create_event(
|
||||
"Recipe Created (URL)",
|
||||
"Recipe Created",
|
||||
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
|
||||
)
|
||||
|
||||
return self.item
|
||||
|
||||
def update_recipe(self, update_data: Recipe) -> Recipe:
|
||||
def update_one(self, update_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
@@ -74,7 +79,7 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
|
||||
return self.item
|
||||
|
||||
def patch_recipe(self, patch_data: Recipe) -> Recipe:
|
||||
def patch_one(self, patch_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
@@ -88,16 +93,7 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
|
||||
return self.item
|
||||
|
||||
def delete_recipe(self) -> Recipe:
|
||||
"""removes a recipe from the database and purges the existing files from the filesystem.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 Bad Request
|
||||
|
||||
Returns:
|
||||
Recipe: The deleted recipe
|
||||
"""
|
||||
|
||||
def delete_one(self) -> Recipe:
|
||||
try:
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug)
|
||||
self._delete_assets()
|
||||
|
||||
Reference in New Issue
Block a user