refactor(backend): ♻️ change error messages to follow standard pattern to match locals on frontend (#668)

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-09-02 21:33:18 -08:00 committed by GitHub
parent abc0d0d59f
commit b550dae593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 415 additions and 72 deletions

View File

@ -0,0 +1,159 @@
import json
import re
from dataclasses import dataclass
from pathlib import Path
from slugify import slugify
CWD = Path(__file__).parent
PROJECT_BASE = CWD.parent.parent
server_side_msgs = PROJECT_BASE / "mealie" / "utils" / "error_messages.py"
en_us_msgs = PROJECT_BASE / "frontend" / "lang" / "errors" / "en-US.json"
client_side_msgs = PROJECT_BASE / "frontend" / "utils" / "error-messages.ts"
GENERATE_MESSAGES = [
# User Related
"user",
"webhook",
"token",
# Group Related
"group",
"cookbook",
"mealplan",
# Recipe Related
"scraper",
"recipe",
"ingredient",
"food",
"unit",
# Admin Related
"backup",
"migration",
"event",
]
class ErrorMessage:
def __init__(self, prefix, verb) -> None:
self.message = f"{prefix.title()} {verb.title()} Failed"
self.snake = slugify(self.message, separator="_")
self.kabab = slugify(self.message, separator="-")
def factory(prefix) -> list["ErrorMessage"]:
verbs = ["Create", "Update", "Delete"]
return [ErrorMessage(prefix, verb) for verb in verbs]
@dataclass
class CodeGenLines:
start: int
end: int
indentation: str
text: list[str]
_next_line = None
def purge_lines(self) -> None:
start = self.start + 1
end = self.end
del self.text[start:end]
def push_line(self, string: str) -> None:
self._next_line = self._next_line or self.start + 1
self.text.insert(self._next_line, self.indentation + string)
self._next_line += 1
def find_start(file_text: list[str], gen_id: str):
for x, line in enumerate(file_text):
if "CODE_GEN_ID:" in line and gen_id in line:
return x, line
return None
def find_end(file_text: list[str], gen_id: str):
for x, line in enumerate(file_text):
if f"END {gen_id}" in line:
return x, line
return None
def get_indentation_of_string(line: str):
return re.sub(r"#.*", "", line).removesuffix("\n")
def get_messages(message_prefix: str) -> str:
prefix = message_prefix.lower()
return [
f'{prefix}_create_failure = "{prefix}-create-failure"\n',
f'{prefix}_update_failure = "{prefix}-update-failure"\n',
f'{prefix}_delete_failure = "{prefix}-delete-failure"\n',
]
def code_gen_factory(file_path: Path) -> CodeGenLines:
with open(file_path, "r") as file:
text = file.readlines()
start_num, line = find_start(text, "ERROR_MESSAGE_ENUMS")
indentation = get_indentation_of_string(line)
end_num, line = find_end(text, "ERROR_MESSAGE_ENUMS")
return CodeGenLines(
start=start_num,
end=end_num,
indentation=indentation,
text=text,
)
def write_to_locals(messages: list[ErrorMessage]) -> None:
with open(en_us_msgs, "r") as f:
existing_msg = json.loads(f.read())
for msg in messages:
if msg.kabab in existing_msg:
continue
existing_msg[msg.kabab] = msg.message
print(f"Added Key {msg.kabab} to 'en-US.json'")
with open(en_us_msgs, "w") as f:
f.write(json.dumps(existing_msg, indent=4))
def main():
print("Starting...")
GENERATE_MESSAGES.sort()
code_gen = code_gen_factory(server_side_msgs)
code_gen.purge_lines()
messages = []
for msg_type in GENERATE_MESSAGES:
messages += get_messages(msg_type)
messages.append("\n")
for msg in messages:
code_gen.push_line(msg)
with open(server_side_msgs, "w") as file:
file.writelines(code_gen.text)
# Locals
local_msgs = []
for msg_type in GENERATE_MESSAGES:
local_msgs += ErrorMessage.factory(msg_type)
write_to_locals(local_msgs)
print("Done!")
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
{
"backup-create-failed": "Backup Create Failed",
"backup-update-failed": "Backup Update Failed",
"backup-delete-failed": "Backup Delete Failed",
"cookbook-create-failed": "Cookbook Create Failed",
"cookbook-update-failed": "Cookbook Update Failed",
"cookbook-delete-failed": "Cookbook Delete Failed",
"event-create-failed": "Event Create Failed",
"event-update-failed": "Event Update Failed",
"event-delete-failed": "Event Delete Failed",
"food-create-failed": "Food Create Failed",
"food-update-failed": "Food Update Failed",
"food-delete-failed": "Food Delete Failed",
"group-create-failed": "Group Create Failed",
"group-update-failed": "Group Update Failed",
"group-delete-failed": "Group Delete Failed",
"ingredient-create-failed": "Ingredient Create Failed",
"ingredient-update-failed": "Ingredient Update Failed",
"ingredient-delete-failed": "Ingredient Delete Failed",
"mealplan-create-failed": "Mealplan Create Failed",
"mealplan-update-failed": "Mealplan Update Failed",
"mealplan-delete-failed": "Mealplan Delete Failed",
"migration-create-failed": "Migration Create Failed",
"migration-update-failed": "Migration Update Failed",
"migration-delete-failed": "Migration Delete Failed",
"recipe-create-failed": "Recipe Create Failed",
"recipe-update-failed": "Recipe Update Failed",
"recipe-delete-failed": "Recipe Delete Failed",
"scraper-create-failed": "Scraper Create Failed",
"scraper-update-failed": "Scraper Update Failed",
"scraper-delete-failed": "Scraper Delete Failed",
"token-create-failed": "Token Create Failed",
"token-update-failed": "Token Update Failed",
"token-delete-failed": "Token Delete Failed",
"unit-create-failed": "Unit Create Failed",
"unit-update-failed": "Unit Update Failed",
"unit-delete-failed": "Unit Delete Failed",
"user-create-failed": "User Create Failed",
"user-update-failed": "User Update Failed",
"user-delete-failed": "User Delete Failed",
"webhook-create-failed": "Webhook Create Failed",
"webhook-update-failed": "Webhook Update Failed",
"webhook-delete-failed": "Webhook Delete Failed"
}

View File

@ -3,12 +3,12 @@ from sqlalchemy.orm.session import Session
from mealie.schema.user.user import PrivateUser from mealie.schema.user.user import PrivateUser
from .dependencies import generate_session, get_current_user, is_logged_in from .dependencies import generate_session, get_admin_user, get_current_user, is_logged_in
class ReadDeps: class PublicDeps:
""" """
ReadDeps contains the common dependencies for all read operations through the API. PublicDeps contains the common dependencies for all read operations through the API.
Note: The user object is used to definer what assets the user has access to. Note: The user object is used to definer what assets the user has access to.
Args: Args:
@ -28,9 +28,9 @@ class ReadDeps:
self.user: bool = user self.user: bool = user
class WriteDeps: class UserDeps:
""" """
WriteDeps contains the common dependencies for all read operations through the API. UserDeps contains the common dependencies for all read operations through the API.
Note: The user must be logged in or the route will return a 401 error. Note: The user must be logged in or the route will return a 401 error.
Args: Args:
@ -48,3 +48,15 @@ class WriteDeps:
self.session: Session = session self.session: Session = session
self.bg_task: BackgroundTasks = background_tasks self.bg_task: BackgroundTasks = background_tasks
self.user: PrivateUser = user self.user: PrivateUser = user
class AdminDeps:
def __init__(
self,
background_tasks: BackgroundTasks,
session: Session = Depends(generate_session),
user=Depends(get_admin_user),
):
self.session: Session = session
self.bg_task: BackgroundTasks = background_tasks
self.user: PrivateUser = user

View File

@ -1,11 +1,11 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Callable, Generic, TypeVar from typing import Callable, Generic, Type, TypeVar
from fastapi import BackgroundTasks, Depends, HTTPException, status from fastapi import BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs, get_settings from mealie.core.config import get_app_dirs, get_settings
from mealie.core.dependencies.grouped import ReadDeps, WriteDeps from mealie.core.dependencies.grouped import PublicDeps, UserDeps
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.db.database import get_database from mealie.db.database import get_database
from mealie.db.db_setup import SessionLocal from mealie.db.db_setup import SessionLocal
@ -17,25 +17,25 @@ T = TypeVar("T")
D = TypeVar("D") D = TypeVar("D")
CLS_DEP = TypeVar("CLS_DEP") # Generic Used for the class method dependencies
class BaseHttpService(Generic[T, D], ABC): class BaseHttpService(Generic[T, D], ABC):
"""The BaseHttpService class is a generic class that can be used to create """
The BaseHttpService class is a generic class that can be used to create
http services that are injected via `Depends` into a route function. To use, http services that are injected via `Depends` into a route function. To use,
you must define the Generic type arguments: you must define the Generic type arguments:
`T`: The type passed into the *_existing functions (e.g. id) which is then passed into assert_existing `T`: The type passed into the *_existing functions (e.g. id) which is then passed into assert_existing
`D`: Item returned from database layer `D`: Item returned from database layer
Child Requirements:
Define the following functions:
`assert_existing(self, data: T) -> None:`
Define the following variables:
`event_func`: A function that is called when an event is created.
""" """
item: D = None item: D = None
# Function that Generate Corrsesponding Routes through RouterFactor # Function that Generate Corrsesponding Routes through RouterFactory:
# if the method is defined or != `None` than the corresponding route is defined through the RouterFactory.
# If the method is not defined, then the route will be excluded from creation. This service based articheture
# is being adopted as apart of the v1 migration
get_all: Callable = None get_all: Callable = None
create_one: Callable = None create_one: Callable = None
update_one: Callable = None update_one: Callable = None
@ -75,48 +75,35 @@ class BaseHttpService(Generic[T, D], ABC):
self._group_id_cache = group.id self._group_id_cache = group.id
return self._group_id_cache return self._group_id_cache
@classmethod def _existing_factory(dependency: Type[CLS_DEP]) -> classmethod:
def read_existing(cls, item_id: T, deps: ReadDeps = Depends()): def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)):
"""
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
"""
new_class = cls(deps.session, deps.user, deps.bg_task) new_class = cls(deps.session, deps.user, deps.bg_task)
new_class.assert_existing(item_id) new_class.assert_existing(item_id)
return new_class return new_class
@classmethod return classmethod(cls_method)
def write_existing(cls, item_id: T, deps: WriteDeps = Depends()):
"""
Used for dependency injection for routes that require an existing recipe. The only difference between
read_existing and write_existing is that the user is required to be logged in on write_existing method.
"""
new_class = cls(deps.session, deps.user, deps.bg_task)
new_class.assert_existing(item_id)
return new_class
@classmethod def _class_method_factory(dependency: Type[CLS_DEP]) -> classmethod:
def public(cls, deps: ReadDeps = Depends()): def cls_method(cls, deps: CLS_DEP = Depends(dependency)):
"""
A Base instance to be used as a router dependency
"""
return cls(deps.session, deps.user, deps.bg_task) return cls(deps.session, deps.user, deps.bg_task)
@classmethod return classmethod(cls_method)
def private(cls, deps: WriteDeps = Depends()):
"""
A Base instance to be used as a router dependency
"""
return cls(deps.session, deps.user, deps.bg_task)
@abstractmethod # TODO: Refactor to allow for configurable dependencies base on substantiation
def populate_item(self) -> None: read_existing = _existing_factory(PublicDeps)
... write_existing = _existing_factory(UserDeps)
public = _class_method_factory(PublicDeps)
private = _class_method_factory(UserDeps)
def assert_existing(self, id: T) -> None: def assert_existing(self, id: T) -> None:
self.populate_item(id) self.populate_item(id)
self._check_item() self._check_item()
@abstractmethod
def populate_item(self) -> None:
...
def _check_item(self) -> None: def _check_item(self) -> None:
if not self.item: if not self.item:
raise HTTPException(status.HTTP_404_NOT_FOUND) raise HTTPException(status.HTTP_404_NOT_FOUND)

View File

@ -0,0 +1,60 @@
from abc import abstractmethod
from typing import TypeVar
from mealie.core.dependencies.grouped import AdminDeps, PublicDeps, UserDeps
from .base_http_service import BaseHttpService
T = TypeVar("T")
D = TypeVar("D")
class PublicHttpService(BaseHttpService[T, D]):
"""
PublicHttpService sets the class methods to PublicDeps for read actions
and UserDeps for write actions which are inaccessible to not logged in users.
"""
read_existing = BaseHttpService._existing_factory(PublicDeps)
write_existing = BaseHttpService._existing_factory(UserDeps)
public = BaseHttpService._class_method_factory(PublicDeps)
private = BaseHttpService._class_method_factory(UserDeps)
@abstractmethod
def populate_item(self) -> None:
...
class UserHttpService(BaseHttpService[T, D]):
"""
UserHttpService sets the class methods to UserDeps which are inaccessible
to not logged in users.
"""
read_existing = BaseHttpService._existing_factory(UserDeps)
write_existing = BaseHttpService._existing_factory(UserDeps)
public = BaseHttpService._class_method_factory(UserDeps)
private = BaseHttpService._class_method_factory(UserDeps)
@abstractmethod
def populate_item(self) -> None:
...
class AdminHttpService(BaseHttpService[T, D]):
"""
AdminHttpService restricts the class methods to AdminDeps which are restricts
all class methods to users who are administrators.
"""
read_existing = BaseHttpService._existing_factory(AdminDeps)
write_existing = BaseHttpService._existing_factory(AdminDeps)
public = BaseHttpService._class_method_factory(AdminDeps)
private = BaseHttpService._class_method_factory(AdminDeps)
@abstractmethod
def populate_item(self) -> None:
...

View File

@ -1,3 +1,4 @@
import inspect
from typing import Any, Callable, Optional, Sequence, Type, TypeVar from typing import Any, Callable, Optional, Sequence, Type, TypeVar
from fastapi import APIRouter from fastapi import APIRouter
@ -23,15 +24,7 @@ class RouterFactory(APIRouter):
update_schema: Type[T] update_schema: Type[T]
_base_path: str = "/" _base_path: str = "/"
def __init__( def __init__(self, service: Type[S], prefix: Optional[str] = None, tags: Optional[list[str]] = None, *_, **kwargs):
self,
service: Type[S],
prefix: Optional[str] = None,
tags: Optional[list[str]] = None,
*args,
**kwargs,
):
self.service: Type[S] = service self.service: Type[S] = service
self.schema: Type[T] = service._schema self.schema: Type[T] = service._schema
@ -57,6 +50,7 @@ class RouterFactory(APIRouter):
methods=["GET"], methods=["GET"],
response_model=Optional[list[self.schema]], # type: ignore response_model=Optional[list[self.schema]], # type: ignore
summary="Get All", summary="Get All",
description=inspect.cleandoc(self.service.get_all.__doc__ or ""),
) )
if self.service.create_one: if self.service.create_one:
@ -66,6 +60,7 @@ class RouterFactory(APIRouter):
methods=["POST"], methods=["POST"],
response_model=self.schema, response_model=self.schema,
summary="Create One", summary="Create One",
description=inspect.cleandoc(self.service.create_one.__doc__ or ""),
) )
if self.service.update_many: if self.service.update_many:
@ -75,6 +70,7 @@ class RouterFactory(APIRouter):
methods=["PUT"], methods=["PUT"],
response_model=Optional[list[self.schema]], # type: ignore response_model=Optional[list[self.schema]], # type: ignore
summary="Update Many", summary="Update Many",
description=inspect.cleandoc(self.service.update_many.__doc__ or ""),
) )
if self.service.delete_all: if self.service.delete_all:
@ -84,6 +80,7 @@ class RouterFactory(APIRouter):
methods=["DELETE"], methods=["DELETE"],
response_model=Optional[list[self.schema]], # type: ignore response_model=Optional[list[self.schema]], # type: ignore
summary="Delete All", summary="Delete All",
description=inspect.cleandoc(self.service.delete_all.__doc__ or ""),
) )
if self.service.populate_item: if self.service.populate_item:
@ -93,6 +90,7 @@ class RouterFactory(APIRouter):
methods=["GET"], methods=["GET"],
response_model=self.get_one_schema, response_model=self.get_one_schema,
summary="Get One", summary="Get One",
description=inspect.cleandoc(self.service.populate_item.__doc__ or ""),
) )
if self.service.update_one: if self.service.update_one:
@ -102,15 +100,18 @@ class RouterFactory(APIRouter):
methods=["PUT"], methods=["PUT"],
response_model=self.schema, response_model=self.schema,
summary="Update One", summary="Update One",
description=inspect.cleandoc(self.service.update_one.__doc__ or ""),
) )
if self.service.delete_one: if self.service.delete_one:
print(self.service.delete_one.__doc__)
self._add_api_route( self._add_api_route(
"/{item_id}", "/{item_id}",
self._delete_one(), self._delete_one(),
methods=["DELETE"], methods=["DELETE"],
response_model=self.schema, response_model=self.schema,
summary="Delete One", summary="Delete One",
description=inspect.cleandoc(self.service.delete_one.__doc__ or ""),
) )
def _add_api_route(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None: def _add_api_route(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:

View File

@ -4,13 +4,13 @@ from fastapi import HTTPException, status
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
from mealie.services.base_http_service.base_http_service import BaseHttpService from mealie.services.base_http_service.http_services import UserHttpService
from mealie.services.events import create_group_event from mealie.services.events import create_group_event
logger = get_logger(module=__name__) logger = get_logger(module=__name__)
class CookbookService(BaseHttpService[int, ReadCookBook]): class CookbookService(UserHttpService[int, ReadCookBook]):
event_func = create_group_event event_func = create_group_event
_restrict_by_group = True _restrict_by_group = True
@ -19,17 +19,17 @@ class CookbookService(BaseHttpService[int, ReadCookBook]):
_update_schema = UpdateCookBook _update_schema = UpdateCookBook
_get_one_schema = RecipeCookBook _get_one_schema = RecipeCookBook
def populate_item(self, id: int | str): def populate_item(self, item_id: int | str):
try: try:
id = int(id) item_id = int(item_id)
except Exception: except Exception:
pass pass
if isinstance(id, int): if isinstance(item_id, int):
self.item = self.db.cookbooks.get_one(self.session, id, override_schema=RecipeCookBook) self.item = self.db.cookbooks.get_one(self.session, item_id, override_schema=RecipeCookBook)
else: else:
self.item = self.db.cookbooks.get_one(self.session, id, key="slug", override_schema=RecipeCookBook) self.item = self.db.cookbooks.get_one(self.session, item_id, key="slug", override_schema=RecipeCookBook)
def get_all(self) -> list[ReadCookBook]: def get_all(self) -> list[ReadCookBook]:
items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999) items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from mealie.core.dependencies.grouped import WriteDeps from mealie.core.dependencies.grouped import UserDeps
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.schema.recipe.recipe_category import CategoryBase from mealie.schema.recipe.recipe_category import CategoryBase
from mealie.schema.user.user import GroupInDB from mealie.schema.user.user import GroupInDB
@ -18,12 +18,12 @@ class GroupSelfService(BaseHttpService[int, str]):
item: GroupInDB item: GroupInDB
@classmethod @classmethod
def read_existing(cls, deps: WriteDeps = Depends()): def read_existing(cls, deps: UserDeps = Depends()):
"""Override parent method to remove `item_id` from arguments""" """Override parent method to remove `item_id` from arguments"""
return super().read_existing(item_id=0, deps=deps) return super().read_existing(item_id=0, deps=deps)
@classmethod @classmethod
def write_existing(cls, deps: WriteDeps = Depends()): def write_existing(cls, deps: UserDeps = Depends()):
"""Override parent method to remove `item_id` from arguments""" """Override parent method to remove `item_id` from arguments"""
return super().write_existing(item_id=0, deps=deps) return super().write_existing(item_id=0, deps=deps)

View File

@ -5,7 +5,7 @@ from typing import Union
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from mealie.core.dependencies.grouped import ReadDeps, WriteDeps from mealie.core.dependencies.grouped import PublicDeps, UserDeps
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.schema.recipe.recipe import CreateRecipe, Recipe from mealie.schema.recipe.recipe import CreateRecipe, Recipe
from mealie.services.base_http_service.base_http_service import BaseHttpService from mealie.services.base_http_service.base_http_service import BaseHttpService
@ -25,11 +25,11 @@ class RecipeService(BaseHttpService[str, Recipe]):
event_func = create_recipe_event event_func = create_recipe_event
@classmethod @classmethod
def write_existing(cls, slug: str, deps: WriteDeps = Depends()): def write_existing(cls, slug: str, deps: UserDeps = Depends()):
return super().write_existing(slug, deps) return super().write_existing(slug, deps)
@classmethod @classmethod
def read_existing(cls, slug: str, deps: ReadDeps = Depends()): def read_existing(cls, slug: str, deps: PublicDeps = Depends()):
return super().write_existing(slug, deps) return super().write_existing(slug, deps)
def assert_existing(self, slug: str): def assert_existing(self, slug: str):

View File

@ -0,0 +1,81 @@
from dataclasses import dataclass
@dataclass
class ErrorMessages:
"""
This enum class holds the text values that represent the errors returned when
something goes wrong on the server side.
Example: {"details": "general-failure"}
The items contained within the '#' are automatically generated by a script in the scripts directory.
DO NOT EDIT THE CONTENTS BETWEEN THOSE. If you need to add a custom error message, do so in the lines
above.
Why Generate This!?!?! If we generate static errors on the backend we can ensure that a constant
set or error messages will be returned to the frontend. As such we can use the "details" key to
look up localized messages in the frontend. as such DO NOT change the generated or manual codes
without making the necessary changes on the client side code.
"""
general_failure = "general-failure"
# CODE_GEN_ID: ERROR_MESSAGE_ENUMS
backup_create_failure = "backup-create-failure"
backup_update_failure = "backup-update-failure"
backup_delete_failure = "backup-delete-failure"
cookbook_create_failure = "cookbook-create-failure"
cookbook_update_failure = "cookbook-update-failure"
cookbook_delete_failure = "cookbook-delete-failure"
event_create_failure = "event-create-failure"
event_update_failure = "event-update-failure"
event_delete_failure = "event-delete-failure"
food_create_failure = "food-create-failure"
food_update_failure = "food-update-failure"
food_delete_failure = "food-delete-failure"
group_create_failure = "group-create-failure"
group_update_failure = "group-update-failure"
group_delete_failure = "group-delete-failure"
ingredient_create_failure = "ingredient-create-failure"
ingredient_update_failure = "ingredient-update-failure"
ingredient_delete_failure = "ingredient-delete-failure"
mealplan_create_failure = "mealplan-create-failure"
mealplan_update_failure = "mealplan-update-failure"
mealplan_delete_failure = "mealplan-delete-failure"
migration_create_failure = "migration-create-failure"
migration_update_failure = "migration-update-failure"
migration_delete_failure = "migration-delete-failure"
recipe_create_failure = "recipe-create-failure"
recipe_update_failure = "recipe-update-failure"
recipe_delete_failure = "recipe-delete-failure"
scraper_create_failure = "scraper-create-failure"
scraper_update_failure = "scraper-update-failure"
scraper_delete_failure = "scraper-delete-failure"
token_create_failure = "token-create-failure"
token_update_failure = "token-update-failure"
token_delete_failure = "token-delete-failure"
unit_create_failure = "unit-create-failure"
unit_update_failure = "unit-update-failure"
unit_delete_failure = "unit-delete-failure"
user_create_failure = "user-create-failure"
user_update_failure = "user-update-failure"
user_delete_failure = "user-delete-failure"
webhook_create_failure = "webhook-create-failure"
webhook_update_failure = "webhook-update-failure"
webhook_delete_failure = "webhook-delete-failure"
# END ERROR_MESSAGE_ENUMS