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:
Hayden
2021-09-09 08:51:29 -08:00
committed by GitHub
parent 3c504e7048
commit bdaf758712
90 changed files with 1793 additions and 949 deletions

View File

@@ -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")

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

View File

@@ -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()