1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-05 21:45:25 +02:00

feat: server side search (#2112) (#2117)

* feat: server side search API (#2112)

* refactor repository_recipes filter building

* add food filter to recipe repository page_all

* fix query type annotations

* working search

* add tests and make sure title matches are ordered correctly

* remove instruction matching again

* fix formatting and small issues

* fix another linting error

* make search test no rely on actual words

* fix failing postgres compiled query

* revise incorrectly ordered migration

* automatically extract latest migration version

* test migration orderes

* run type generators

* new search function

* wip: new search page

* sortable field options

* fix virtual scroll issue

* fix search casing bug

* finalize search filters/sorts

* remove old composable

* fix type errors

---------

Co-authored-by: Sören <fleshgolem@gmx.net>
This commit is contained in:
Hayden 2023-02-11 21:26:10 -09:00 committed by GitHub
parent fc105dcebc
commit 71f8c1066a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1057 additions and 822 deletions

View file

@ -2,9 +2,11 @@ from typing import cast
from mealie.repos.repository_factory import AllRepositories
from mealie.repos.repository_recipes import RepositoryRecipes
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipePaginationQuery, RecipeSummary
from mealie.schema.recipe import RecipeIngredient, SaveIngredientFood, RecipeStep
from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipeSummary
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagSave
from mealie.schema.recipe.recipe_tool import RecipeToolSave
from mealie.schema.response import OrderDirection, PaginationQuery
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
@ -164,7 +166,7 @@ def test_recipe_repo_pagination_by_categories(database: AllRepositories, unique_
for recipe in recipes:
database.recipes.create(recipe)
pagination_query = RecipePaginationQuery(
pagination_query = PaginationQuery(
page=1,
per_page=-1,
)
@ -245,7 +247,7 @@ def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user:
for recipe in recipes:
database.recipes.create(recipe)
pagination_query = RecipePaginationQuery(
pagination_query = PaginationQuery(
page=1,
per_page=-1,
)
@ -324,7 +326,7 @@ def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user:
for recipe in recipes:
database.recipes.create(recipe)
pagination_query = RecipePaginationQuery(
pagination_query = PaginationQuery(
page=1,
per_page=-1,
)
@ -357,3 +359,138 @@ def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user:
tool_ids = [tool.id for tool in recipe_summary.tools]
for tool in created_tools:
assert tool.id in tool_ids
def test_recipe_repo_pagination_by_foods(database: AllRepositories, unique_user: TestUser):
slug1, slug2 = (random_string(10) for _ in range(2))
foods = [
SaveIngredientFood(group_id=unique_user.group_id, name=slug1),
SaveIngredientFood(group_id=unique_user.group_id, name=slug2),
]
created_foods = [database.ingredient_foods.create(food) for food in foods]
# Bootstrap the database with recipes
recipes = []
for i in range(10):
# None of the foods
recipes.append(
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=random_string(),
)
)
# Only one of the foods
recipes.append(
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=random_string(),
recipe_ingredient=[RecipeIngredient(food=created_foods[i % 2])],
),
)
# Both of the foods
recipes.append(
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=random_string(),
recipe_ingredient=[RecipeIngredient(food=created_foods[0]), RecipeIngredient(food=created_foods[1])],
)
)
for recipe in recipes:
database.recipes.create(recipe)
pagination_query = PaginationQuery(
page=1,
per_page=-1,
)
# Get all recipes with only one food by UUID
food_id = created_foods[0].id
recipes_with_one_food = database.recipes.page_all(pagination_query, foods=[food_id]).items
assert len(recipes_with_one_food) == 15
# Get all recipes with both foods
recipes_with_both_foods = database.recipes.page_all(
pagination_query, foods=[food.id for food in created_foods]
).items
assert len(recipes_with_both_foods) == 10
# Get all recipes with either foods
recipes_with_either_food = database.recipes.page_all(
pagination_query, foods=[food.id for food in created_foods], require_all_foods=False
).items
assert len(recipes_with_either_food) == 20
def test_recipe_repo_search(database: AllRepositories, unique_user: TestUser):
ingredient_1 = random_string(10)
ingredient_2 = random_string(10)
name_part_1 = random_string(10)
name_1 = f"{name_part_1} soup"
name_part_2 = random_string(10)
name_2 = f"Rustic {name_part_2} stew"
name_3 = f"{ingredient_1} Soup"
description_part_1 = random_string(10)
recipes = [
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=name_1,
description=f"My favorite {description_part_1}",
recipe_ingredient=[
RecipeIngredient(note=ingredient_1),
],
),
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=name_2,
recipe_ingredient=[
RecipeIngredient(note=ingredient_2),
],
),
Recipe(
user_id=unique_user.user_id,
group_id=unique_user.group_id,
name=name_3,
),
]
for recipe in recipes:
database.recipes.create(recipe)
pagination_query = PaginationQuery(page=1, per_page=-1, order_by="created_at", order_direction=OrderDirection.asc)
# No hits
empty_result = database.recipes.page_all(pagination_query, search=random_string(10)).items
assert len(empty_result) == 0
# Search by title
title_result = database.recipes.page_all(pagination_query, search=name_part_2).items
assert len(title_result) == 1
assert title_result[0].name == name_2
# Search by description
description_result = database.recipes.page_all(pagination_query, search=description_part_1).items
assert len(description_result) == 1
assert description_result[0].name == name_1
# Search by ingredient
ingredient_result = database.recipes.page_all(pagination_query, search=ingredient_2).items
assert len(ingredient_result) == 1
assert ingredient_result[0].name == name_2
# Make sure title matches are ordered in front
ordered_result = database.recipes.page_all(pagination_query, search=ingredient_1).items
assert len(ordered_result) == 2
assert ordered_result[0].name == name_3
assert ordered_result[1].name == name_1

View file

@ -2,10 +2,7 @@ import json
from mealie.core.config import get_app_settings
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
ALEMBIC_VERSIONS = [
{"version_num": "ff5f73b01a7a"},
]
from tests.utils.alembic_reader import alembic_versions
def test_alchemy_exporter():
@ -13,16 +10,16 @@ def test_alchemy_exporter():
exporter = AlchemyExporter(settings.DB_URL)
data = exporter.dump()
assert data["alembic_version"] == ALEMBIC_VERSIONS
assert data["alembic_version"] == alembic_versions()
assert json.dumps(data, indent=4) # Make sure data is json-serializable
def test_validate_schemas():
schema = {
"alembic_version": ALEMBIC_VERSIONS,
"alembic_version": alembic_versions(),
}
match = {
"alembic_version": ALEMBIC_VERSIONS,
"alembic_version": alembic_versions(),
}
invalid_version = {
@ -33,7 +30,7 @@ def test_validate_schemas():
assert not AlchemyExporter.validate_schemas(schema, invalid_version)
schema_with_tables = {
"alembic_version": ALEMBIC_VERSIONS,
"alembic_version": alembic_versions(),
"recipes": [
{
"id": 1,
@ -41,7 +38,7 @@ def test_validate_schemas():
],
}
match_with_tables = {
"alembic_version": ALEMBIC_VERSIONS,
"alembic_version": alembic_versions(),
"recipes": [
{
"id": 2,
@ -50,3 +47,5 @@ def test_validate_schemas():
}
assert AlchemyExporter.validate_schemas(schema_with_tables, match_with_tables)
assert AlchemyExporter.validate_schemas(schema_with_tables, match_with_tables)
assert AlchemyExporter.validate_schemas(schema_with_tables, match_with_tables)

View file

@ -1,14 +1,47 @@
import pytest
import pathlib
# Test that alembic revisions are applicable and result in the current database
# See https://github.com/sqlalchemy/alembic/issues/724 for inspiration
from pydantic import BaseModel
from tests.utils.alembic_reader import ALEMBIC_MIGRATIONS, import_file
@pytest.mark.skip("TODO: Implement")
def test_alembic_revisions_are_applicable():
pass
class AlembicMigration(BaseModel):
path: pathlib.Path
revision: str | None
down_revision: str | None
@pytest.mark.skip("TODO: Implement")
def test_alembic_revisions_are_up_to_date():
pass
def test_alembic_revisions_are_in_order() -> None:
# read all files
paths = sorted(ALEMBIC_MIGRATIONS.glob("*.py"))
# convert to sorted list of AlembicMigration
migrations: list[AlembicMigration] = []
for path in paths:
mod = import_file("alembic_version", path)
revision = getattr(mod, "revision", None)
down_revision = getattr(mod, "down_revision", None)
migrations.append(
AlembicMigration(
path=path,
revision=revision,
down_revision=down_revision,
)
)
# step through each migration and check
# - revision is in order
# - down_revision is in order
# - down_revision is the previous revision
last = None
for migration in migrations:
if last is not None:
assert (
last.revision == migration.down_revision
), f"{last.revision} != {migration.down_revision} for {migration.path}"
last = migration
last = migration