diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..b000f8fd --- /dev/null +++ b/alembic.ini @@ -0,0 +1,72 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d:%%(minute).2d:%%(second).2d_%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. Valid values are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # default: use os.pathsep + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# Will be replaced in alembic/env.py with the actual url +sqlalchemy.url = + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +hooks = isort, black + +# format using "isort" - use the console_scripts runner, against the "isort" entrypoint +isort.type = console_scripts +isort.entrypoint = isort +isort.options = REVISION_SCRIPT_FILENAME + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +black.type = console_scripts +black.entrypoint = black +black.options = REVISION_SCRIPT_FILENAME diff --git a/alembic/README b/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..27ddad4a --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,77 @@ +from sqlalchemy import engine_from_config, pool + +import mealie.db.models._all_models # noqa: F401 +from alembic import context +from mealie.core.config import get_app_settings +from mealie.db.models._model_base import SqlAlchemyBase + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = SqlAlchemyBase.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + +# Set DB url from config +settings = get_app_settings() +config.set_main_option("sqlalchemy.url", settings.DB_URL) + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata, user_module_prefix="mealie.db.migration_types." + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..881fa315 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +import sqlalchemy as sa + +import mealie.db.migration_types +from alembic import op +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/2022-02-21-19:56:24_6b0f5f32d602_initial_tables.py b/alembic/versions/2022-02-21-19:56:24_6b0f5f32d602_initial_tables.py new file mode 100644 index 00000000..03cd0918 --- /dev/null +++ b/alembic/versions/2022-02-21-19:56:24_6b0f5f32d602_initial_tables.py @@ -0,0 +1,920 @@ +"""Initial tables + +Revision ID: 6b0f5f32d602 +Revises: +Create Date: 2022-02-21 19:56:24.351115 + +""" +import sqlalchemy as sa +from sqlalchemy import engine_from_config + +import mealie.db.migration_types +from alembic import op + +# revision identifiers, used by Alembic. +revision = "6b0f5f32d602" +down_revision = None +branch_labels = None +depends_on = None + + +# Adapted from https://improveandrepeat.com/2021/09/python-friday-87-handling-pre-existing-tables-with-alembic-and-sqlalchemy/ +def table_exists(table, schema=None): + config = op.get_context().config + engine = engine_from_config(config.get_section(config.config_ini_section), prefix="sqlalchemy.") + insp = sa.inspect(engine) + return insp.has_table(table, schema) + + +def upgrade(): + # Only create initial tables if they don't exist yet, to ease transition from pre-alembic state + if table_exists("users"): + return + + op.create_table( + "groups", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_groups_name"), "groups", ["name"], unique=True) + op.create_table( + "categories", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("slug", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("slug", "group_id", name="category_slug_group_id_key"), + ) + op.create_index(op.f("ix_categories_group_id"), "categories", ["group_id"], unique=False) + op.create_index(op.f("ix_categories_name"), "categories", ["name"], unique=False) + op.create_index(op.f("ix_categories_slug"), "categories", ["slug"], unique=False) + op.create_table( + "cookbooks", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("position", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("slug", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "group_data_exports", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("filename", sa.String(), nullable=False), + sa.Column("path", sa.String(), nullable=False), + sa.Column("size", sa.String(), nullable=False), + sa.Column("expires", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_group_data_exports_group_id"), "group_data_exports", ["group_id"], unique=False) + op.create_table( + "group_events_notifiers", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("apprise_url", sa.String(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_group_events_notifiers_group_id"), "group_events_notifiers", ["group_id"], unique=False) + op.create_table( + "group_meal_plan_rules", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("day", sa.String(), nullable=False), + sa.Column("entry_type", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "group_preferences", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("private_group", sa.Boolean(), nullable=True), + sa.Column("first_day_of_week", sa.Integer(), nullable=True), + sa.Column("recipe_public", sa.Boolean(), nullable=True), + sa.Column("recipe_show_nutrition", sa.Boolean(), nullable=True), + sa.Column("recipe_show_assets", sa.Boolean(), nullable=True), + sa.Column("recipe_landscape_view", sa.Boolean(), nullable=True), + sa.Column("recipe_disable_comments", sa.Boolean(), nullable=True), + sa.Column("recipe_disable_amount", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_group_preferences_group_id"), "group_preferences", ["group_id"], unique=False) + op.create_table( + "group_reports", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("category", sa.String(), nullable=False), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_group_reports_category"), "group_reports", ["category"], unique=False) + op.create_index(op.f("ix_group_reports_group_id"), "group_reports", ["group_id"], unique=False) + op.create_table( + "ingredient_units", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("abbreviation", sa.String(), nullable=True), + sa.Column("fraction", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "invite_tokens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("token", sa.String(), nullable=False), + sa.Column("uses_left", sa.Integer(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_invite_tokens_token"), "invite_tokens", ["token"], unique=True) + op.create_table( + "multi_purpose_labels", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("color", sa.String(length=10), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_multi_purpose_labels_group_id"), "multi_purpose_labels", ["group_id"], unique=False) + op.create_table( + "recipes", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("slug", sa.String(), nullable=True), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("image", sa.String(), nullable=True), + sa.Column("total_time", sa.String(), nullable=True), + sa.Column("prep_time", sa.String(), nullable=True), + sa.Column("perform_time", sa.String(), nullable=True), + sa.Column("cook_time", sa.String(), nullable=True), + sa.Column("recipe_yield", sa.String(), nullable=True), + sa.Column("recipeCuisine", sa.String(), nullable=True), + sa.Column("rating", sa.Integer(), nullable=True), + sa.Column("org_url", sa.String(), nullable=True), + sa.Column("date_added", sa.Date(), nullable=True), + sa.Column("date_updated", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], use_alter=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"), + ) + op.create_index(op.f("ix_recipes_group_id"), "recipes", ["group_id"], unique=False) + op.create_index(op.f("ix_recipes_slug"), "recipes", ["slug"], unique=False) + op.create_index(op.f("ix_recipes_user_id"), "recipes", ["user_id"], unique=False) + op.create_table( + "server_tasks", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("completed_date", sa.DateTime(), nullable=True), + sa.Column("status", sa.String(), nullable=False), + sa.Column("log", sa.String(), nullable=True), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_server_tasks_group_id"), "server_tasks", ["group_id"], unique=False) + op.create_table( + "shopping_lists", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_shopping_lists_group_id"), "shopping_lists", ["group_id"], unique=False) + op.create_table( + "tags", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("slug", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"), + ) + op.create_index(op.f("ix_tags_group_id"), "tags", ["group_id"], unique=False) + op.create_index(op.f("ix_tags_name"), "tags", ["name"], unique=False) + op.create_index(op.f("ix_tags_slug"), "tags", ["slug"], unique=False) + op.create_table( + "tools", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("slug", sa.String(), nullable=False), + sa.Column("on_hand", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"), + ) + op.create_index(op.f("ix_tools_name"), "tools", ["name"], unique=True) + op.create_index(op.f("ix_tools_slug"), "tools", ["slug"], unique=True) + op.create_table( + "webhook_urls", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("enabled", sa.Boolean(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("url", sa.String(), nullable=True), + sa.Column("time", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_webhook_urls_group_id"), "webhook_urls", ["group_id"], unique=False) + op.create_table( + "api_extras", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("recipee_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("key_name", sa.String(), nullable=True), + sa.Column("value", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["recipee_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "cookbooks_to_categories", + sa.Column("cookbook_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("category_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["categories.id"], + ), + sa.ForeignKeyConstraint( + ["cookbook_id"], + ["cookbooks.id"], + ), + ) + op.create_table( + "group_events_notifier_options", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("event_notifier_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("recipe_created", sa.Boolean(), nullable=False), + sa.Column("recipe_updated", sa.Boolean(), nullable=False), + sa.Column("recipe_deleted", sa.Boolean(), nullable=False), + sa.Column("user_signup", sa.Boolean(), nullable=False), + sa.Column("data_migrations", sa.Boolean(), nullable=False), + sa.Column("data_export", sa.Boolean(), nullable=False), + sa.Column("data_import", sa.Boolean(), nullable=False), + sa.Column("mealplan_entry_created", sa.Boolean(), nullable=False), + sa.Column("shopping_list_created", sa.Boolean(), nullable=False), + sa.Column("shopping_list_updated", sa.Boolean(), nullable=False), + sa.Column("shopping_list_deleted", sa.Boolean(), nullable=False), + sa.Column("cookbook_created", sa.Boolean(), nullable=False), + sa.Column("cookbook_updated", sa.Boolean(), nullable=False), + sa.Column("cookbook_deleted", sa.Boolean(), nullable=False), + sa.Column("tag_created", sa.Boolean(), nullable=False), + sa.Column("tag_updated", sa.Boolean(), nullable=False), + sa.Column("tag_deleted", sa.Boolean(), nullable=False), + sa.Column("category_created", sa.Boolean(), nullable=False), + sa.Column("category_updated", sa.Boolean(), nullable=False), + sa.Column("category_deleted", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ["event_notifier_id"], + ["group_events_notifiers.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "group_meal_plans", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("date", sa.Date(), nullable=False), + sa.Column("entry_type", sa.String(), nullable=False), + sa.Column("title", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_group_meal_plans_date"), "group_meal_plans", ["date"], unique=False) + op.create_index(op.f("ix_group_meal_plans_entry_type"), "group_meal_plans", ["entry_type"], unique=False) + op.create_index(op.f("ix_group_meal_plans_group_id"), "group_meal_plans", ["group_id"], unique=False) + op.create_index(op.f("ix_group_meal_plans_recipe_id"), "group_meal_plans", ["recipe_id"], unique=False) + op.create_index(op.f("ix_group_meal_plans_title"), "group_meal_plans", ["title"], unique=False) + op.create_table( + "group_to_categories", + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("category_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["categories.id"], + ), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + ) + op.create_table( + "ingredient_foods", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("label_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.ForeignKeyConstraint( + ["label_id"], + ["multi_purpose_labels.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "notes", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("title", sa.String(), nullable=True), + sa.Column("text", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "plan_rules_to_categories", + sa.Column("group_plan_rule_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("category_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["categories.id"], + ), + sa.ForeignKeyConstraint( + ["group_plan_rule_id"], + ["group_meal_plan_rules.id"], + ), + ) + op.create_table( + "plan_rules_to_tags", + sa.Column("plan_rule_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("tag_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["plan_rule_id"], + ["group_meal_plan_rules.id"], + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["tags.id"], + ), + ) + op.create_table( + "recipe_assets", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("icon", sa.String(), nullable=True), + sa.Column("file_name", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recipe_instructions", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("position", sa.Integer(), nullable=True), + sa.Column("type", sa.String(), nullable=True), + sa.Column("title", sa.String(), nullable=True), + sa.Column("text", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recipe_nutrition", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("calories", sa.String(), nullable=True), + sa.Column("fat_content", sa.String(), nullable=True), + sa.Column("fiber_content", sa.String(), nullable=True), + sa.Column("protein_content", sa.String(), nullable=True), + sa.Column("carbohydrate_content", sa.String(), nullable=True), + sa.Column("sodium_content", sa.String(), nullable=True), + sa.Column("sugar_content", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recipe_settings", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("public", sa.Boolean(), nullable=True), + sa.Column("show_nutrition", sa.Boolean(), nullable=True), + sa.Column("show_assets", sa.Boolean(), nullable=True), + sa.Column("landscape_view", sa.Boolean(), nullable=True), + sa.Column("disable_amount", sa.Boolean(), nullable=True), + sa.Column("disable_comments", sa.Boolean(), nullable=True), + sa.Column("locked", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recipe_share_tokens", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("expires_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_recipe_share_tokens_group_id"), "recipe_share_tokens", ["group_id"], unique=False) + op.create_table( + "recipes_to_categories", + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("category_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["categories.id"], + ), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + ) + op.create_table( + "recipes_to_tags", + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("tag_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["tags.id"], + ), + ) + op.create_table( + "recipes_to_tools", + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("tool_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["tool_id"], + ["tools.id"], + ), + ) + op.create_table( + "report_entries", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("success", sa.Boolean(), nullable=True), + sa.Column("message", sa.String(), nullable=True), + sa.Column("exception", sa.String(), nullable=True), + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("report_id", mealie.db.migration_types.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["report_id"], + ["group_reports.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "shopping_list_recipe_reference", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("shopping_list_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("recipe_quantity", sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["shopping_list_id"], + ["shopping_lists.id"], + ), + sa.PrimaryKeyConstraint("id", "shopping_list_id"), + ) + op.create_index( + op.f("ix_shopping_list_recipe_reference_recipe_id"), + "shopping_list_recipe_reference", + ["recipe_id"], + unique=False, + ) + op.create_table( + "users", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("full_name", sa.String(), nullable=True), + sa.Column("username", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=True), + sa.Column("password", sa.String(), nullable=True), + sa.Column("admin", sa.Boolean(), nullable=True), + sa.Column("advanced", sa.Boolean(), nullable=True), + sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("cache_key", sa.String(), nullable=True), + sa.Column("can_manage", sa.Boolean(), nullable=True), + sa.Column("can_invite", sa.Boolean(), nullable=True), + sa.Column("can_organize", sa.Boolean(), nullable=True), + sa.Column("owned_recipes_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["group_id"], + ["groups.id"], + ), + sa.ForeignKeyConstraint( + ["owned_recipes_id"], + ["recipes.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) + op.create_index(op.f("ix_users_full_name"), "users", ["full_name"], unique=False) + op.create_index(op.f("ix_users_group_id"), "users", ["group_id"], unique=False) + op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True) + op.create_table( + "long_live_tokens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("token", sa.String(), nullable=False), + sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "password_reset_tokens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("token", sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("token"), + ) + op.create_table( + "recipe_comments", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("text", sa.String(), nullable=True), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recipe_ingredient_ref_link", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("instruction_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("reference_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["instruction_id"], + ["recipe_instructions.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recipes_ingredients", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("position", sa.Integer(), nullable=True), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("title", sa.String(), nullable=True), + sa.Column("note", sa.String(), nullable=True), + sa.Column("unit_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("food_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("quantity", sa.Integer(), nullable=True), + sa.Column("reference_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["food_id"], + ["ingredient_foods.id"], + ), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["unit_id"], + ["ingredient_units.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "shopping_list_items", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("shopping_list_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("is_ingredient", sa.Boolean(), nullable=True), + sa.Column("position", sa.Integer(), nullable=False), + sa.Column("checked", sa.Boolean(), nullable=True), + sa.Column("quantity", sa.Float(), nullable=True), + sa.Column("note", sa.String(), nullable=True), + sa.Column("is_food", sa.Boolean(), nullable=True), + sa.Column("unit_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("food_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("label_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["food_id"], + ["ingredient_foods.id"], + ), + sa.ForeignKeyConstraint( + ["label_id"], + ["multi_purpose_labels.id"], + ), + sa.ForeignKeyConstraint( + ["shopping_list_id"], + ["shopping_lists.id"], + ), + sa.ForeignKeyConstraint( + ["unit_id"], + ["ingredient_units.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "users_to_favorites", + sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + ) + op.create_table( + "shopping_list_item_recipe_reference", + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("shopping_list_item_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("recipe_id", mealie.db.migration_types.GUID(), nullable=True), + sa.Column("recipe_quantity", sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ["recipe_id"], + ["recipes.id"], + ), + sa.ForeignKeyConstraint( + ["shopping_list_item_id"], + ["shopping_list_items.id"], + ), + sa.PrimaryKeyConstraint("id", "shopping_list_item_id"), + ) + op.create_index( + op.f("ix_shopping_list_item_recipe_reference_recipe_id"), + "shopping_list_item_recipe_reference", + ["recipe_id"], + unique=False, + ) + + +def downgrade(): + op.drop_index( + op.f("ix_shopping_list_item_recipe_reference_recipe_id"), table_name="shopping_list_item_recipe_reference" + ) + op.drop_table("shopping_list_item_recipe_reference") + op.drop_table("users_to_favorites") + op.drop_table("shopping_list_items") + op.drop_table("recipes_ingredients") + op.drop_table("recipe_ingredient_ref_link") + op.drop_table("recipe_comments") + op.drop_table("password_reset_tokens") + op.drop_table("long_live_tokens") + op.drop_index(op.f("ix_users_username"), table_name="users") + op.drop_index(op.f("ix_users_group_id"), table_name="users") + op.drop_index(op.f("ix_users_full_name"), table_name="users") + op.drop_index(op.f("ix_users_email"), table_name="users") + op.drop_table("users") + op.drop_index(op.f("ix_shopping_list_recipe_reference_recipe_id"), table_name="shopping_list_recipe_reference") + op.drop_table("shopping_list_recipe_reference") + op.drop_table("report_entries") + op.drop_table("recipes_to_tools") + op.drop_table("recipes_to_tags") + op.drop_table("recipes_to_categories") + op.drop_index(op.f("ix_recipe_share_tokens_group_id"), table_name="recipe_share_tokens") + op.drop_table("recipe_share_tokens") + op.drop_table("recipe_settings") + op.drop_table("recipe_nutrition") + op.drop_table("recipe_instructions") + op.drop_table("recipe_assets") + op.drop_table("plan_rules_to_tags") + op.drop_table("plan_rules_to_categories") + op.drop_table("notes") + op.drop_table("ingredient_foods") + op.drop_table("group_to_categories") + op.drop_index(op.f("ix_group_meal_plans_title"), table_name="group_meal_plans") + op.drop_index(op.f("ix_group_meal_plans_recipe_id"), table_name="group_meal_plans") + op.drop_index(op.f("ix_group_meal_plans_group_id"), table_name="group_meal_plans") + op.drop_index(op.f("ix_group_meal_plans_entry_type"), table_name="group_meal_plans") + op.drop_index(op.f("ix_group_meal_plans_date"), table_name="group_meal_plans") + op.drop_table("group_meal_plans") + op.drop_table("group_events_notifier_options") + op.drop_table("cookbooks_to_categories") + op.drop_table("api_extras") + op.drop_index(op.f("ix_webhook_urls_group_id"), table_name="webhook_urls") + op.drop_table("webhook_urls") + op.drop_index(op.f("ix_tools_slug"), table_name="tools") + op.drop_index(op.f("ix_tools_name"), table_name="tools") + op.drop_table("tools") + op.drop_index(op.f("ix_tags_slug"), table_name="tags") + op.drop_index(op.f("ix_tags_name"), table_name="tags") + op.drop_index(op.f("ix_tags_group_id"), table_name="tags") + op.drop_table("tags") + op.drop_index(op.f("ix_shopping_lists_group_id"), table_name="shopping_lists") + op.drop_table("shopping_lists") + op.drop_index(op.f("ix_server_tasks_group_id"), table_name="server_tasks") + op.drop_table("server_tasks") + op.drop_index(op.f("ix_recipes_user_id"), table_name="recipes") + op.drop_index(op.f("ix_recipes_slug"), table_name="recipes") + op.drop_index(op.f("ix_recipes_group_id"), table_name="recipes") + op.drop_table("recipes") + op.drop_index(op.f("ix_multi_purpose_labels_group_id"), table_name="multi_purpose_labels") + op.drop_table("multi_purpose_labels") + op.drop_index(op.f("ix_invite_tokens_token"), table_name="invite_tokens") + op.drop_table("invite_tokens") + op.drop_table("ingredient_units") + op.drop_index(op.f("ix_group_reports_group_id"), table_name="group_reports") + op.drop_index(op.f("ix_group_reports_category"), table_name="group_reports") + op.drop_table("group_reports") + op.drop_index(op.f("ix_group_preferences_group_id"), table_name="group_preferences") + op.drop_table("group_preferences") + op.drop_table("group_meal_plan_rules") + op.drop_index(op.f("ix_group_events_notifiers_group_id"), table_name="group_events_notifiers") + op.drop_table("group_events_notifiers") + op.drop_index(op.f("ix_group_data_exports_group_id"), table_name="group_data_exports") + op.drop_table("group_data_exports") + op.drop_table("cookbooks") + op.drop_index(op.f("ix_categories_slug"), table_name="categories") + op.drop_index(op.f("ix_categories_name"), table_name="categories") + op.drop_index(op.f("ix_categories_group_id"), table_name="categories") + op.drop_table("categories") + op.drop_index(op.f("ix_groups_name"), table_name="groups") + op.drop_table("groups") diff --git a/mealie/core/config.py b/mealie/core/config.py index 896eba2e..0d5a3352 100644 --- a/mealie/core/config.py +++ b/mealie/core/config.py @@ -18,7 +18,7 @@ ENV = BASE_DIR.joinpath(".env") dotenv.load_dotenv(ENV) PRODUCTION = os.getenv("PRODUCTION", "True").lower() in ["true", "1"] -TESTING = os.getenv("TESTING", "True").lower() in ["true", "1"] +TESTING = os.getenv("TESTING", "False").lower() in ["true", "1"] def determine_data_dir() -> Path: diff --git a/mealie/db/init_db.py b/mealie/db/init_db.py index 64260407..d1a5e74d 100644 --- a/mealie/db/init_db.py +++ b/mealie/db/init_db.py @@ -1,7 +1,13 @@ +from pathlib import Path + +from sqlalchemy import engine + +from alembic import command, config, script +from alembic.config import Config +from alembic.runtime import migration from mealie.core import root_logger from mealie.core.config import get_app_settings -from mealie.db.db_setup import create_session, engine -from mealie.db.models._model_base import SqlAlchemyBase +from mealie.db.db_setup import create_session from mealie.repos.all_repositories import get_repositories from mealie.repos.repository_factory import AllRepositories from mealie.repos.seed.init_users import default_user_init @@ -9,15 +15,11 @@ from mealie.repos.seed.seeders import IngredientFoodsSeeder, IngredientUnitsSeed from mealie.schema.user.user import GroupBase from mealie.services.group_services.group_utils import create_new_group +PROJECT_DIR = Path(__file__).parent.parent.parent + logger = root_logger.get_logger("init_db") -def create_all_models(): - import mealie.db.models._all_models # noqa: F401 - - SqlAlchemyBase.metadata.create_all(engine) - - def init_db(db: AllRepositories) -> None: # TODO: Port other seed data to use abstract seeder class default_group_init(db) @@ -42,24 +44,33 @@ def default_group_init(db: AllRepositories): create_new_group(db, GroupBase(name=settings.DEFAULT_GROUP)) +# Adapted from https://alembic.sqlalchemy.org/en/latest/cookbook.html#test-current-database-revision-is-at-head-s +def db_is_at_head(alembic_cfg: config.Config) -> bool: + settings = get_app_settings() + url = settings.DB_URL + connectable = engine.create_engine(url) + directory = script.ScriptDirectory.from_config(alembic_cfg) + with connectable.begin() as connection: + context = migration.MigrationContext.configure(connection) + return set(context.get_current_heads()) == set(directory.get_heads()) + + def main(): - create_all_models() + alembic_cfg = Config(str(PROJECT_DIR / "alembic.ini")) + if db_is_at_head(alembic_cfg): + logger.info("Migration not needed.") + else: + logger.info("Migration needed. Performing migration...") + command.upgrade(alembic_cfg, "head") session = create_session() db = get_repositories(session) - try: - init_user = db.users.get_all() - if not init_user: - raise Exception("No users found in database") - except Exception: - init_db(db) - return - + init_user = db.users.get_all() if init_user: - logger.info("Database Exists") + logger.info("Database exists") else: - logger.info("Database Doesn't Exists, Initializing...") + logger.info("Database contains no users, initializing...") init_db(db) diff --git a/mealie/db/migration_types.py b/mealie/db/migration_types.py new file mode 100644 index 00000000..7ce86b58 --- /dev/null +++ b/mealie/db/migration_types.py @@ -0,0 +1 @@ +from mealie.db.models._model_utils.guid import GUID # noqa: F401 diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py index 0367748d..e7d4ad73 100644 --- a/mealie/db/models/recipe/recipe.py +++ b/mealie/db/models/recipe/recipe.py @@ -10,10 +10,11 @@ from mealie.db.models._model_utils.guid import GUID from .._model_base import BaseMixins, SqlAlchemyBase from .._model_utils import auto_init -from ..users import users_to_favorites +from ..users.user_to_favorite import users_to_favorites from .api_extras import ApiExtras from .assets import RecipeAsset from .category import recipes_to_categories +from .comment import RecipeComment from .ingredient import RecipeIngredient from .instruction import RecipeInstruction from .note import Note @@ -50,7 +51,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True) group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id]) - user_id = sa.Column(GUID, sa.ForeignKey("users.id"), index=True) + user_id = sa.Column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True) user = orm.relationship("User", uselist=False, foreign_keys=[user_id]) meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan") @@ -93,7 +94,9 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): RecipeShareTokenModel, back_populates="recipe", cascade="all, delete, delete-orphan" ) - comments: list = orm.relationship("RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan") + comments: list[RecipeComment] = orm.relationship( + "RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan" + ) # Mealie Specific settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan") diff --git a/mealie/db/models/users/users.py b/mealie/db/models/users/users.py index 66494921..b55758c9 100644 --- a/mealie/db/models/users/users.py +++ b/mealie/db/models/users/users.py @@ -5,7 +5,6 @@ from mealie.db.models._model_utils.guid import GUID from .._model_base import BaseMixins, SqlAlchemyBase from .._model_utils import auto_init -from ..group import Group from .user_to_favorite import users_to_favorites @@ -74,6 +73,8 @@ class User(SqlAlchemyBase, BaseMixins): settings = get_app_settings() group = settings.DEFAULT_GROUP + from mealie.db.models.group import Group + self.group = Group.get_ref(session, group) self.favorite_recipes = [] @@ -91,6 +92,8 @@ class User(SqlAlchemyBase, BaseMixins): self.full_name = full_name self.email = email + from mealie.db.models.group import Group + self.group = Group.get_ref(session, group) if self.username is None: diff --git a/poetry.lock b/poetry.lock index 4760106c..38775b3d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,21 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "alembic" +version = "1.7.5" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + [[package]] name = "aniso8601" version = "7.0.0" @@ -585,6 +600,21 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] +[[package]] +name = "mako" +version = "1.1.6" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + [[package]] name = "markdown" version = "3.3.6" @@ -1458,13 +1488,17 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "869d87ae4fb7dff15f0b53dddde737ce8a6c2b13217d6690f4e2b90d3198a18c" +content-hash = "94bda5d85d0fd3f5fb0c3ce59ff7cf515eaa3210c4533862e1c8bd22eecec8dd" [metadata.files] aiofiles = [ {file = "aiofiles-0.5.0-py3-none-any.whl", hash = "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb"}, {file = "aiofiles-0.5.0.tar.gz", hash = "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af"}, ] +alembic = [ + {file = "alembic-1.7.5-py3-none-any.whl", hash = "sha256:a9dde941534e3d7573d9644e8ea62a2953541e27bc1793e166f60b777ae098b4"}, + {file = "alembic-1.7.5.tar.gz", hash = "sha256:7c328694a2e68f03ee971e63c3bd885846470373a5b532cf2c9f1601c413b153"}, +] aniso8601 = [ {file = "aniso8601-7.0.0-py2.py3-none-any.whl", hash = "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"}, {file = "aniso8601-7.0.0.tar.gz", hash = "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e"}, @@ -1703,6 +1737,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, @@ -1715,6 +1750,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, @@ -1723,6 +1759,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, @@ -1731,6 +1768,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, @@ -1739,6 +1777,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, @@ -1904,6 +1943,10 @@ lxml = [ {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] +mako = [ + {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, + {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, +] markdown = [ {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, diff --git a/pyproject.toml b/pyproject.toml index 55467b22..ccf4bd89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ fastapi = "^0.71.0" uvicorn = {extras = ["standard"], version = "^0.13.0"} APScheduler = "^3.8.1" SQLAlchemy = "^1.4.29" +alembic = "^1.7.5" Jinja2 = "^2.11.2" python-dotenv = "^0.15.0" python-slugify = "^4.0.1" diff --git a/tests/unit_tests/test_alembic.py b/tests/unit_tests/test_alembic.py new file mode 100644 index 00000000..331785d6 --- /dev/null +++ b/tests/unit_tests/test_alembic.py @@ -0,0 +1,14 @@ +import pytest + +# Test that alembic revisions are applicable and result in the current database +# See https://github.com/sqlalchemy/alembic/issues/724 for inspiration + + +@pytest.mark.skip("TODO: Implement") +def test_alembic_revisions_are_applicable(): + pass + + +@pytest.mark.skip("TODO: Implement") +def test_alembic_revisions_are_up_to_date(): + pass diff --git a/tests/unit_tests/test_init_db.py b/tests/unit_tests/test_init_db.py deleted file mode 100644 index e69de29b..00000000