1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-02 20:15:24 +02:00

feature: proper multi-tenant-support (#969)(WIP)

* update naming

* refactor tests to use shared structure

* shorten names

* add tools test case

* refactor to support multi-tenant

* set group_id on creation

* initial refactor for multitenant tags/cats

* spelling

* additional test case for same valued resources

* fix recipe update tests

* apply indexes to foreign keys

* fix performance regressions

* handle unknown exception

* utility decorator for function debugging

* migrate recipe_id to UUID

* GUID for recipes

* remove unused import

* move image functions into package

* move utilities to packages dir

* update import

* linter

* image image and asset routes

* update assets and images to use UUIDs

* fix migration base

* image asset test coverage

* use ids for categories and tag crud functions

* refactor recipe organizer test suite to reduce duplication

* add uuid serlization utility

* organizer base router

* slug routes testing and fixes

* fix postgres error

* adopt UUIDs

* move tags, categories, and tools under "organizers" umbrella

* update composite label

* generate ts types

* fix import error

* update frontend types

* fix type errors

* fix postgres errors

* fix #978

* add null check for title validation

* add note in docs on multi-tenancy
This commit is contained in:
Hayden 2022-02-13 12:23:42 -09:00 committed by GitHub
parent 9a82a172cb
commit c617251f4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
157 changed files with 1866 additions and 1578 deletions

View file

@ -1,47 +0,0 @@
import { BaseCRUDAPI } from "../_base";
import { Recipe } from "~/types/api-types/recipe";
const prefix = "/api";
export interface Category {
name: string;
id: number;
slug: string;
recipes?: Recipe[];
}
export interface CreateCategory {
name: string;
}
const routes = {
categories: `${prefix}/categories`,
categoriesEmpty: `${prefix}/categories/empty`,
categoriesCategory: (category: string) => `${prefix}/categories/${category}`,
};
export class CategoriesAPI extends BaseCRUDAPI<Category, CreateCategory> {
baseRoute: string = routes.categories;
itemRoute = routes.categoriesCategory;
/** Returns a list of categories that do not contain any recipes
*/
async getEmptyCategories() {
return await this.requests.get(routes.categoriesEmpty);
}
/** Returns a list of recipes associated with the provided category.
*/
async getAllRecipesByCategory(category: string) {
return await this.requests.get(routes.categoriesCategory(category));
}
/** Removes a recipe category from the database. Deleting a
* category does not impact a recipe. The category will be removed
* from any recipes that contain it
*/
async deleteRecipeCategory(category: string) {
return await this.requests.delete(routes.categoriesCategory(category));
}
}

View file

@ -1,6 +1,6 @@
import { BaseCRUDAPI } from "../_base";
import { Category } from "./categories";
import { CategoryBase } from "~/types/api-types/recipe";
import { RecipeCategory } from "~/types/api-types/user";
const prefix = "/api";
@ -14,7 +14,7 @@ export interface CookBook extends CreateCookBook {
description: string;
position: number;
group_id: number;
categories: Category[] | CategoryBase[];
categories: RecipeCategory[] | CategoryBase[];
}
const routes = {

View file

@ -16,7 +16,7 @@ export interface CreateMealPlan {
entryType: PlanEntryType;
title: string;
text: string;
recipeId?: number;
recipeId?: string;
}
export interface UpdateMealPlan extends CreateMealPlan {

View file

@ -12,7 +12,7 @@ const prefix = "/api";
const routes = {
shoppingLists: `${prefix}/groups/shopping/lists`,
shoppingListsId: (id: string) => `${prefix}/groups/shopping/lists/${id}`,
shoppingListIdAddRecipe: (id: string, recipeId: number) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/groups/shopping/lists/${id}/recipe/${recipeId}`,
shoppingListItems: `${prefix}/groups/shopping/items`,
shoppingListItemsId: (id: string) => `${prefix}/groups/shopping/items/${id}`,
@ -22,11 +22,11 @@ export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListOut, ShoppingListC
baseRoute = routes.shoppingLists;
itemRoute = routes.shoppingListsId;
async addRecipe(itemId: string, recipeId: number) {
async addRecipe(itemId: string, recipeId: string) {
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {});
}
async removeRecipe(itemId: string, recipeId: number) {
async removeRecipe(itemId: string, recipeId: string) {
return await this.requests.delete(routes.shoppingListIdAddRecipe(itemId, recipeId));
}
}

View file

@ -0,0 +1,20 @@
import { BaseCRUDAPI } from "../_base";
import { CategoryIn, RecipeCategoryResponse } from "~/types/api-types/recipe";
import { config } from "~/api/config";
const prefix = config.PREFIX + "/organizers";
const routes = {
categories: `${prefix}/categories`,
categoriesId: (category: string) => `${prefix}/categories/${category}`,
categoriesSlug: (category: string) => `${prefix}/categories/slug/${category}`,
};
export class CategoriesAPI extends BaseCRUDAPI<RecipeCategoryResponse, CategoryIn> {
baseRoute: string = routes.categories;
itemRoute = routes.categoriesId;
async bySlug(slug: string) {
return await this.requests.get<RecipeCategoryResponse>(routes.categoriesSlug(slug));
}
}

View file

@ -0,0 +1,20 @@
import { BaseCRUDAPI } from "../_base";
import { RecipeTagResponse, TagIn } from "~/types/api-types/recipe";
import { config } from "~/api/config";
const prefix = config.PREFIX + "/organizers";
const routes = {
tags: `${prefix}/tags`,
tagsId: (tag: string) => `${prefix}/tags/${tag}`,
tagsSlug: (tag: string) => `${prefix}/tags/slug/${tag}`,
};
export class TagsAPI extends BaseCRUDAPI<RecipeTagResponse, TagIn> {
baseRoute: string = routes.tags;
itemRoute = routes.tagsId;
async bySlug(slug: string) {
return await this.requests.get<RecipeTagResponse>(routes.tagsSlug(slug));
}
}

View file

@ -1,7 +1,9 @@
import { BaseCRUDAPI } from "../_base";
import { RecipeTool, RecipeToolCreate, RecipeToolResponse } from "~/types/api-types/recipe";
const prefix = "/api";
import { config } from "~/api/config";
const prefix = config.PREFIX + "/organizers";
const routes = {
tools: `${prefix}/tools`,
@ -13,7 +15,7 @@ export class ToolsApi extends BaseCRUDAPI<RecipeTool, RecipeToolCreate> {
baseRoute: string = routes.tools;
itemRoute = routes.toolsId;
async byslug(slug: string) {
async bySlug(slug: string) {
return await this.requests.get<RecipeToolResponse>(routes.toolsSlug(slug));
}
}

View file

@ -1,22 +1,14 @@
import { BaseCRUDAPI } from "../_base";
import { CreateIngredientFood, IngredientFood } from "~/types/api-types/recipe";
const prefix = "/api";
export interface CreateFood {
name: string;
description: string;
}
export interface Food extends CreateFood {
id: number;
}
const routes = {
food: `${prefix}/foods`,
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
};
export class FoodAPI extends BaseCRUDAPI<Food, CreateFood> {
export class FoodAPI extends BaseCRUDAPI<IngredientFood, CreateIngredientFood> {
baseRoute: string = routes.food;
itemRoute = routes.foodsFood;
}

View file

@ -8,12 +8,12 @@ const routes = {
};
export interface RecipeShareTokenCreate {
recipeId: number;
recipeId: string;
expiresAt?: Date;
}
export interface RecipeShareToken {
recipeId: number;
recipeId: string;
id: string;
groupId: number;
expiresAt: string;

View file

@ -1,6 +1,5 @@
import { Category } from "../categories";
import { Tag } from "../tags";
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
export type Parser = "nlp" | "brute";
@ -30,8 +29,8 @@ export interface ParsedIngredient {
export interface BulkCreateRecipe {
url: string;
categories: Category[];
tags: Tag[];
categories: RecipeCategory[];
tags: RecipeTag[];
}
export interface BulkCreatePayload {
@ -50,7 +49,7 @@ export interface CreateAsset {
}
export interface RecipeCommentCreate {
recipeId: number;
recipeId: string;
text: string;
}

View file

@ -1,47 +0,0 @@
import { BaseCRUDAPI } from "../_base";
import { Recipe } from "~/types/api-types/admin";
const prefix = "/api";
export interface Tag {
name: string;
id: number;
slug: string;
recipes?: Recipe[];
}
export interface CreateTag {
name: string;
}
const routes = {
tags: `${prefix}/tags`,
tagsEmpty: `${prefix}/tags/empty`,
tagsTag: (tag: string) => `${prefix}/tags/${tag}`,
};
export class TagsAPI extends BaseCRUDAPI<Tag, CreateTag> {
baseRoute: string = routes.tags;
itemRoute = routes.tagsTag;
/** Returns a list of categories that do not contain any recipes
*/
async getEmptyCategories() {
return await this.requests.get(routes.tagsEmpty);
}
/** Returns a list of recipes associated with the provided category.
*/
async getAllRecipesByCategory(category: string) {
return await this.requests.get(routes.tagsTag(category));
}
/** Removes a recipe category from the database. Deleting a
* category does not impact a recipe. The category will be removed
* from any recipes that contain it
*/
async deleteRecipeCategory(category: string) {
return await this.requests.delete(routes.tagsTag(category));
}
}

5
frontend/api/config.ts Normal file
View file

@ -0,0 +1,5 @@
const PREFIX = "/api";
export const config = {
PREFIX,
};

View file

@ -4,8 +4,8 @@ import { GroupAPI } from "./class-interfaces/groups";
import { EventsAPI } from "./class-interfaces/events";
import { BackupAPI } from "./class-interfaces/backups";
import { UploadFile } from "./class-interfaces/upload";
import { CategoriesAPI } from "./class-interfaces/categories";
import { TagsAPI } from "./class-interfaces/tags";
import { CategoriesAPI } from "./class-interfaces/organizer-categories";
import { TagsAPI } from "./class-interfaces/organizer-tags";
import { UtilsAPI } from "./class-interfaces/utils";
import { FoodAPI } from "./class-interfaces/recipe-foods";
import { UnitAPI } from "./class-interfaces/recipe-units";
@ -17,7 +17,7 @@ import { EmailAPI } from "./class-interfaces/email";
import { BulkActionsAPI } from "./class-interfaces/recipe-bulk-actions";
import { GroupServerTaskAPI } from "./class-interfaces/group-tasks";
import { AdminAPI } from "./admin-api";
import { ToolsApi } from "./class-interfaces/tools";
import { ToolsApi } from "./class-interfaces/organizer-tools";
import { GroupMigrationApi } from "./class-interfaces/group-migrations";
import { GroupReportsApi } from "./class-interfaces/group-reports";
import { ShoppingApi } from "./class-interfaces/group-shopping-lists";

View file

@ -24,15 +24,7 @@
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
<v-tooltip v-if="!locked" bottom color="info">
<template #activator="{ on, attrs }">
<v-btn
fab
small
class="mx-1"
color="info"
v-bind="attrs"
v-on="on"
@click="$emit('input', true)"
>
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('input', true)">
<v-icon> {{ $globals.icons.edit }} </v-icon>
</v-btn>
</template>
@ -40,14 +32,7 @@
</v-tooltip>
<v-tooltip v-else bottom color="info">
<template #activator="{ on, attrs }">
<v-btn
fab
small
class="mx-1"
color="info"
v-bind="attrs"
v-on="on"
>
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
<v-icon> {{ $globals.icons.lock }} </v-icon>
</v-btn>
</template>
@ -93,7 +78,7 @@
</template>
<script lang="ts">
import {defineComponent, ref, useContext} from "@nuxtjs/composition-api";
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
@ -123,7 +108,7 @@ export default defineComponent({
},
recipeId: {
required: true,
type: Number,
type: String,
},
locked: {
type: Boolean,
@ -191,7 +176,7 @@ export default defineComponent({
editorButtons,
emitHandler,
emitDelete,
}
};
},
});
</script>

View file

@ -86,6 +86,10 @@ export default defineComponent({
type: String,
required: true,
},
recipeId: {
type: String,
required: true,
},
value: {
type: Array,
required: true,
@ -143,7 +147,7 @@ export default defineComponent({
const { recipeAssetPath } = useStaticRoutes();
function assetURL(assetName: string) {
return recipeAssetPath(props.slug, assetName);
return recipeAssetPath(props.recipeId, assetName);
}
function assetEmbed(name: string) {

View file

@ -8,7 +8,14 @@
:min-height="imageHeight + 75"
@click="$emit('click')"
>
<RecipeCardImage :icon-size="imageHeight" :height="imageHeight" :slug="slug" small :image-version="image">
<RecipeCardImage
:icon-size="imageHeight"
:height="imageHeight"
:slug="slug"
:recipe-id="recipeId"
small
:image-version="image"
>
<v-expand-transition v-if="description">
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
<v-card-text class="v-card--text-show white--text">
@ -95,7 +102,7 @@ export default defineComponent({
},
recipeId: {
required: true,
type: Number,
type: String,
},
imageHeight: {
type: Number,

View file

@ -2,7 +2,7 @@
<v-img
v-if="!fallBackImage"
:height="height"
:src="getImage(slug)"
:src="getImage(recipeId)"
@click="$emit('click')"
@load="fallBackImage = false"
@error="fallBackImage = true"
@ -18,7 +18,7 @@
</template>
<script lang="ts">
import {computed, defineComponent, ref, watch} from "@nuxtjs/composition-api";
import { computed, defineComponent, ref, watch } from "@nuxtjs/composition-api";
import { useStaticRoutes, useUserApi } from "~/composables/api";
export default defineComponent({
@ -43,6 +43,10 @@ export default defineComponent({
type: String,
default: null,
},
recipeId: {
type: String,
required: true,
},
imageVersion: {
type: String,
default: null,
@ -63,20 +67,23 @@ export default defineComponent({
if (props.small) return "small";
if (props.large) return "large";
return "large";
})
watch(() => props.slug, () => {
fallBackImage.value = false;
});
function getImage(slug: string) {
watch(
() => props.recipeId,
() => {
fallBackImage.value = false;
}
);
function getImage(recipeId: string) {
switch (imageSize.value) {
case "tiny":
return recipeTinyImage(slug, props.imageVersion);
return recipeTinyImage(recipeId, props.imageVersion);
case "small":
return recipeSmallImage(slug, props.imageVersion);
return recipeSmallImage(recipeId, props.imageVersion);
case "large":
return recipeImage(slug, props.imageVersion);
return recipeImage(recipeId, props.imageVersion);
}
}

View file

@ -10,7 +10,14 @@
<v-list-item three-line>
<slot name="avatar">
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<RecipeCardImage :icon-size="100" :height="125" :slug="slug" small :image-version="image"></RecipeCardImage>
<RecipeCardImage
:icon-size="100"
:height="125"
:slug="slug"
:recipe-id="recipeId"
small
:image-version="image"
></RecipeCardImage>
</v-list-item-avatar>
</slot>
<v-list-item-content>
@ -93,7 +100,7 @@ export default defineComponent({
default: true,
},
recipeId: {
type: Number,
type: String,
required: true,
},
},
@ -105,7 +112,7 @@ export default defineComponent({
return {
loggedIn,
}
};
},
});
</script>

View file

@ -46,8 +46,7 @@
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
import { useTags, useCategories } from "~/composables/recipes";
import { Category } from "~/api/class-interfaces/categories";
import { Tag } from "~/api/class-interfaces/tags";
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
const MOUNTED_EVENT = "mounted";
@ -57,7 +56,7 @@ export default defineComponent({
},
props: {
value: {
type: Array as () => (Category | Tag | string)[],
type: Array as () => (RecipeTag | RecipeCategory | string)[],
required: true,
},
solo: {
@ -103,9 +102,12 @@ export default defineComponent({
const state = reactive({
selected: props.value,
});
watch(() => props.value, (val) => {
state.selected = val;
});
watch(
() => props.value,
(val) => {
state.selected = val;
}
);
const { i18n } = useContext();
const inputLabel = computed(() => {
@ -114,14 +116,14 @@ export default defineComponent({
});
const activeItems = computed(() => {
let itemObjects: Tag[] | Category[] | null;
let itemObjects: RecipeTag[] | RecipeCategory[] | null;
if (props.tagSelector) itemObjects = allTags.value;
else {
itemObjects = allCategories.value;
}
if (props.returnObject) return itemObjects;
else {
return itemObjects?.map((x: Tag | Category) => x.name);
return itemObjects?.map((x: RecipeTag | RecipeCategory) => x.name);
}
});
@ -145,7 +147,7 @@ export default defineComponent({
state.selected.splice(index, 1);
}
function pushToItem(createdItem: Tag | Category) {
function pushToItem(createdItem: RecipeTag | RecipeCategory) {
// TODO: Remove excessive get calls
getAllCategories();
getAllTags();
@ -164,4 +166,3 @@ export default defineComponent({
},
});
</script>

View file

@ -69,7 +69,7 @@ export default defineComponent({
required: true,
},
recipeId: {
type: Number,
type: String,
required: true,
},
},
@ -114,4 +114,4 @@ export default defineComponent({
return { api, comments, ...toRefs(state), submitComment, deleteComment };
},
});
</script>
</script>

View file

@ -168,7 +168,7 @@ export default defineComponent({
},
recipeId: {
required: true,
type: Number,
type: String,
},
},
setup(props, context) {

View file

@ -70,7 +70,7 @@ export default defineComponent({
default: false,
},
recipeId: {
type: Number,
type: String,
required: true,
},
name: {

View file

@ -158,14 +158,15 @@ export default defineComponent({
}
function handleUnitEnter() {
if (value.unit === undefined || !value.unit.name.includes(unitSearch.value)) {
if (value.unit === undefined || value.unit === null || !value.unit.name.includes(unitSearch.value)) {
console.log("Creating");
createAssignUnit();
}
}
function handleFoodEnter() {
if (value.food === undefined || !value.food.name.includes(foodSearch.value)) {
console.log(value.food);
if (value.food === undefined || value.food === null || !value.food.name.includes(foodSearch.value)) {
console.log("Creating");
createAssignFood();
}
@ -190,7 +191,7 @@ export default defineComponent({
});
</script>
<style >
<style>
.v-input__append-outer {
margin: 0 !important;
padding: 0 !important;

View file

@ -50,7 +50,7 @@ export default defineComponent({
},
setup(props) {
function validateTitle(title?: string) {
return !(title === undefined || title === "");
return !(title === undefined || title === "" || title === null);
}
const state = reactive({

View file

@ -10,20 +10,20 @@ export const useStaticRoutes = () => {
const fullBase = serverBase + prefix;
// Methods to Generate reference urls for assets/images *
function recipeImage(recipeSlug: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
function recipeImage(recipeId: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?&rnd=${key}&version=${version}`;
}
function recipeSmallImage(recipeSlug: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
function recipeSmallImage(recipeId: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?&rnd=${key}&version=${version}`;
}
function recipeTinyImage(recipeSlug: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
function recipeTinyImage(recipeId: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
}
function recipeAssetPath(recipeSlug: string, assetName: string) {
return `${fullBase}/media/recipes/${recipeSlug}/assets/${assetName}`;
function recipeAssetPath(recipeId: string, assetName: string) {
return `${fullBase}/media/recipes/${recipeId}/assets/${assetName}`;
}
return {

View file

@ -12,11 +12,11 @@ export const useFoods = function () {
const deleteTargetId = ref(0);
const validForm = ref(true);
const workingFoodData = reactive({
id: 0,
const workingFoodData = reactive<IngredientFood>({
id: "",
name: "",
description: "",
labelId: "",
labelId: undefined,
});
const actions = {
@ -80,16 +80,16 @@ export const useFoods = function () {
}
},
resetWorking() {
workingFoodData.id = 0;
workingFoodData.id = "";
workingFoodData.name = "";
workingFoodData.description = "";
workingFoodData.labelId = "";
workingFoodData.labelId = undefined;
},
setWorking(item: IngredientFood) {
workingFoodData.id = item.id;
workingFoodData.name = item.name;
workingFoodData.description = item.description || "";
workingFoodData.labelId = item.labelId || "";
workingFoodData.labelId = item.labelId;
},
flushStore() {
foodStore = null;

View file

@ -1,9 +1,7 @@
import { Ref } from "@nuxtjs/composition-api";
// import { useStaticRoutes } from "../api";
import { Recipe } from "~/types/api-types/recipe";
export const useRecipeMeta = (recipe: Ref<Recipe | null>) => {
// const { recipeImage } = useStaticRoutes();
return () => {
const imageURL = "";
return {

View file

@ -2,10 +2,11 @@ import { reactive, ref, useAsync } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
import { RecipeTool } from "~/types/api-types/user";
export const useTools = function (eager = true) {
const workingToolData = reactive({
id: 0,
const workingToolData = reactive<RecipeTool>({
id: "",
name: "",
slug: "",
onHand: false,
@ -72,7 +73,7 @@ export const useTools = function (eager = true) {
reset() {
workingToolData.name = "";
workingToolData.id = 0;
workingToolData.id = "";
loading.value = false;
validForm.value = true;
},

View file

@ -1,13 +1,17 @@
import { Ref, ref, useAsync } from "@nuxtjs/composition-api";
import { useUserApi } from "../api";
import { useAsyncKey } from "../use-utils";
import { CategoriesAPI, Category } from "~/api/class-interfaces/categories";
import { Tag, TagsAPI } from "~/api/class-interfaces/tags";
import { CategoriesAPI } from "~/api/class-interfaces/organizer-categories";
import { TagsAPI } from "~/api/class-interfaces/organizer-tags";
import { RecipeTag, RecipeCategory } from "~/types/api-types/recipe";
export const allCategories = ref<Category[] | null>([]);
export const allTags = ref<Tag[] | null>([]);
export const allCategories = ref<RecipeCategory[] | null>([]);
export const allTags = ref<RecipeTag[] | null>([]);
function baseTagsCategories(reference: Ref<Category[] | null> | Ref<Tag[] | null>, api: TagsAPI | CategoriesAPI) {
function baseTagsCategories(
reference: Ref<RecipeCategory[] | null> | Ref<RecipeTag[] | null>,
api: TagsAPI | CategoriesAPI
) {
function useAsyncGetAll() {
useAsync(async () => {
await refreshItems();

View file

@ -107,6 +107,7 @@
<v-list-item-avatar :rounded="false">
<RecipeCardImage
v-if="mealplan.recipe"
:recipe-id="mealplan.recipe.id"
tiny
icon-size="25"
:slug="mealplan.recipe ? mealplan.recipe.slug : ''"

View file

@ -7,8 +7,8 @@
></RecipeCardSection>
</v-container>
</template>
<script lang="ts">
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useRecipes, recentRecipes } from "~/composables/recipes";
@ -23,4 +23,3 @@ export default defineComponent({
},
});
</script>

View file

@ -35,7 +35,7 @@
:max-width="enableLandscape ? null : '50%'"
min-height="50"
:height="hideImage ? undefined : imageHeight"
:src="recipeImage(recipe.slug, imageKey)"
:src="recipeImage(recipe.id, imageKey)"
class="d-print-none"
@error="hideImage = true"
>
@ -284,6 +284,7 @@
v-model="recipe.assets"
:edit="form"
:slug="recipe.slug"
:recipe-id="recipe.id"
/>
</client-only>
</div>
@ -362,6 +363,7 @@
v-model="recipe.assets"
:edit="form"
:slug="recipe.slug"
:recipe-id="recipe.id"
/>
</client-only>
</div>
@ -562,7 +564,6 @@ export default defineComponent({
const { recipeImage } = useStaticRoutes();
// ===========================================================================
// Layout Helpers
@ -691,9 +692,8 @@ export default defineComponent({
// Recipe Tools
async function updateTool(tool: RecipeTool) {
if (tool.id === undefined)
return;
if (tool.id === undefined) return;
const { response } = await api.tools.updateOne(tool.id, tool);
if (response?.status === 200) {

View file

@ -72,7 +72,7 @@ export default defineComponent({
});
const category = useAsync(async () => {
const { data } = await api.categories.getOne(slug);
const { data } = await api.categories.bySlug(slug);
if (data) {
state.initialValue = data.name;
}
@ -93,7 +93,7 @@ export default defineComponent({
if (!category.value) {
return;
}
const { data } = await api.categories.updateOne(category.value.slug, category.value);
const { data } = await api.categories.updateOne(category.value.id, category.value);
if (data) {
router.push("/recipes/categories/" + data.slug);

View file

@ -72,7 +72,7 @@ export default defineComponent({
});
const tags = useAsync(async () => {
const { data } = await api.tags.getOne(slug);
const { data } = await api.tags.bySlug(slug);
if (data) {
state.initialValue = data.name;
}
@ -93,7 +93,7 @@ export default defineComponent({
if (!tags.value) {
return;
}
const { data } = await api.tags.updateOne(tags.value.slug, tags.value);
const { data } = await api.tags.updateOne(tags.value.id, tags.value);
if (data) {
router.push("/recipes/tags/" + data.slug);

View file

@ -66,7 +66,7 @@ export default defineComponent({
});
const tools = useAsync(async () => {
const { data } = await api.tools.byslug(slug);
const { data } = await api.tools.bySlug(slug);
if (data) {
state.initialValue = data.name;
}

View file

@ -115,8 +115,8 @@ import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategory
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useRecipes, allRecipes, useFoods } from "~/composables/recipes";
import { RecipeSummary } from "~/types/api-types/recipe";
import { Tag } from "~/api/class-interfaces/tags";
import { useRouteQuery } from "~/composables/use-router";
import { RecipeTag } from "~/types/api-types/user";
interface GenericFilter {
exclude: boolean;
@ -189,7 +189,7 @@ export default defineComponent({
state.includeTags,
// @ts-ignore See above
recipe.tags.map((x: Tag) => x.name),
recipe.tags.map((x: RecipeTag) => x.name),
state.tagFilter.matchAny,
state.tagFilter.exclude
);

View file

@ -324,7 +324,7 @@ export default defineComponent({
if (data) {
if (data && data !== undefined) {
console.log("Computed Meta. RefKey=");
const imageURL = data.slug ? recipeImage(data.slug) : undefined;
const imageURL = data.id ? recipeImage(data.id) : undefined;
title.value = data.name;
meta.value = [

View file

@ -411,7 +411,7 @@ export default defineComponent({
return shoppingList.value?.recipeReferences?.map((ref) => ref.recipe) ?? [];
});
async function addRecipeReferenceToList(recipeId: number) {
async function addRecipeReferenceToList(recipeId: string) {
if (!shoppingList.value) {
return;
}
@ -423,7 +423,7 @@ export default defineComponent({
}
}
async function removeRecipeReferenceToList(recipeId: number) {
async function removeRecipeReferenceToList(recipeId: string) {
if (!shoppingList.value) {
return;
}
@ -577,4 +577,3 @@ export default defineComponent({
max-width: 50px;
}
</style>

View file

@ -72,12 +72,12 @@ export interface CustomPageBase {
}
export interface RecipeCategoryResponse {
name: string;
id: number;
id: string;
slug: string;
recipes?: Recipe[];
recipes?: RecipeSummary[];
}
export interface Recipe {
id?: number;
export interface RecipeSummary {
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -97,28 +97,19 @@ export interface Recipe {
recipeIngredient?: RecipeIngredient[];
dateAdded?: string;
dateUpdated?: string;
recipeInstructions?: RecipeStep[];
nutrition?: Nutrition;
settings?: RecipeSettings;
assets?: RecipeAsset[];
notes?: RecipeNote[];
extras?: {
[k: string]: unknown;
};
comments?: RecipeCommentOut[];
}
export interface RecipeCategory {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTool {
id?: number;
id: string;
name: string;
slug: string;
onHand?: boolean;
@ -137,7 +128,7 @@ export interface IngredientUnit {
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
id: string;
}
export interface CreateIngredientUnit {
name: string;
@ -149,7 +140,7 @@ export interface IngredientFood {
name: string;
description?: string;
labelId?: string;
id: number;
id: string;
label?: MultiPurposeLabelSummary;
}
export interface MultiPurposeLabelSummary {
@ -163,59 +154,6 @@ export interface CreateIngredientFood {
description?: string;
labelId?: string;
}
export interface RecipeStep {
id?: string;
title?: string;
text: string;
ingredientReferences?: IngredientReferences[];
}
/**
* A list of ingredient references.
*/
export interface IngredientReferences {
referenceId?: string;
}
export interface Nutrition {
calories?: string;
fatContent?: string;
proteinContent?: string;
carbohydrateContent?: string;
fiberContent?: string;
sodiumContent?: string;
sugarContent?: string;
}
export interface RecipeSettings {
public?: boolean;
showNutrition?: boolean;
showAssets?: boolean;
landscapeView?: boolean;
disableComments?: boolean;
disableAmount?: boolean;
locked?: boolean;
}
export interface RecipeAsset {
name: string;
icon: string;
fileName?: string;
}
export interface RecipeNote {
title: string;
text: string;
}
export interface RecipeCommentOut {
recipeId: number;
text: string;
id: string;
createdAt: string;
updateAt: string;
userId: string;
user: UserBase;
}
export interface UserBase {
id: number;
username?: string;
admin: boolean;
}
export interface CustomPageImport {
name: string;
status: boolean;

View file

@ -7,7 +7,7 @@
export interface CategoryBase {
name: string;
id: number;
id: string;
slug: string;
}
export interface CreateCookBook {
@ -28,12 +28,12 @@ export interface ReadCookBook {
}
export interface RecipeCategoryResponse {
name: string;
id: number;
id: string;
slug: string;
recipes?: Recipe[];
recipes?: RecipeSummary[];
}
export interface Recipe {
id?: number;
export interface RecipeSummary {
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -53,28 +53,19 @@ export interface Recipe {
recipeIngredient?: RecipeIngredient[];
dateAdded?: string;
dateUpdated?: string;
recipeInstructions?: RecipeStep[];
nutrition?: Nutrition;
settings?: RecipeSettings;
assets?: RecipeAsset[];
notes?: RecipeNote[];
extras?: {
[k: string]: unknown;
};
comments?: RecipeCommentOut[];
}
export interface RecipeCategory {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTool {
id?: number;
id: string;
name: string;
slug: string;
onHand?: boolean;
@ -93,7 +84,7 @@ export interface IngredientUnit {
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
id: string;
}
export interface CreateIngredientUnit {
name: string;
@ -105,7 +96,7 @@ export interface IngredientFood {
name: string;
description?: string;
labelId?: string;
id: number;
id: string;
label?: MultiPurposeLabelSummary;
}
export interface MultiPurposeLabelSummary {
@ -119,59 +110,6 @@ export interface CreateIngredientFood {
description?: string;
labelId?: string;
}
export interface RecipeStep {
id?: string;
title?: string;
text: string;
ingredientReferences?: IngredientReferences[];
}
/**
* A list of ingredient references.
*/
export interface IngredientReferences {
referenceId?: string;
}
export interface Nutrition {
calories?: string;
fatContent?: string;
proteinContent?: string;
carbohydrateContent?: string;
fiberContent?: string;
sodiumContent?: string;
sugarContent?: string;
}
export interface RecipeSettings {
public?: boolean;
showNutrition?: boolean;
showAssets?: boolean;
landscapeView?: boolean;
disableComments?: boolean;
disableAmount?: boolean;
locked?: boolean;
}
export interface RecipeAsset {
name: string;
icon: string;
fileName?: string;
}
export interface RecipeNote {
title: string;
text: string;
}
export interface RecipeCommentOut {
recipeId: number;
text: string;
id: string;
createdAt: string;
updateAt: string;
userId: string;
user: UserBase;
}
export interface UserBase {
id: number;
username?: string;
admin: boolean;
}
export interface RecipeCookBook {
name: string;
description?: string;

View file

@ -180,7 +180,7 @@ export interface IngredientFood {
name: string;
description?: string;
labelId?: string;
id: number;
id: string;
label?: MultiPurposeLabelSummary;
}
export interface MultiPurposeLabelSummary {
@ -194,7 +194,7 @@ export interface IngredientUnit {
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
id: string;
}
export interface ReadGroupPreferences {
privateGroup?: boolean;
@ -222,7 +222,7 @@ export interface ReadWebhook {
id: number;
}
export interface RecipeSummary {
id?: number;
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -244,17 +244,17 @@ export interface RecipeSummary {
dateUpdated?: string;
}
export interface RecipeCategory {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTool {
id?: number;
id: string;
name: string;
slug: string;
onHand?: boolean;
@ -307,15 +307,15 @@ export interface ShoppingListItemCreate {
isFood?: boolean;
note?: string;
quantity?: number;
unitId?: number;
unitId?: string;
unit?: IngredientUnit;
foodId?: number;
foodId?: string;
food?: IngredientFood;
labelId?: string;
recipeReferences?: ShoppingListItemRecipeRef[];
}
export interface ShoppingListItemRecipeRef {
recipeId: number;
recipeId: string;
recipeQuantity: number;
}
export interface ShoppingListItemOut {
@ -325,9 +325,9 @@ export interface ShoppingListItemOut {
isFood?: boolean;
note?: string;
quantity?: number;
unitId?: number;
unitId?: string;
unit?: IngredientUnit;
foodId?: number;
foodId?: string;
food?: IngredientFood;
labelId?: string;
recipeReferences?: ShoppingListItemRecipeRefOut[];
@ -335,7 +335,7 @@ export interface ShoppingListItemOut {
label?: MultiPurposeLabelSummary;
}
export interface ShoppingListItemRecipeRefOut {
recipeId: number;
recipeId: string;
recipeQuantity: number;
id: string;
shoppingListItemId: string;
@ -347,9 +347,9 @@ export interface ShoppingListItemUpdate {
isFood?: boolean;
note?: string;
quantity?: number;
unitId?: number;
unitId?: string;
unit?: IngredientUnit;
foodId?: number;
foodId?: string;
food?: IngredientFood;
labelId?: string;
recipeReferences?: ShoppingListItemRecipeRef[];
@ -365,7 +365,7 @@ export interface ShoppingListOut {
export interface ShoppingListRecipeRefOut {
id: string;
shoppingListId: string;
recipeId: number;
recipeId: string;
recipeQuantity: number;
recipe: RecipeSummary;
}

View file

@ -10,7 +10,7 @@ export type PlanRulesDay = "monday" | "tuesday" | "wednesday" | "thursday" | "fr
export type PlanRulesType = "breakfast" | "lunch" | "dinner" | "unset";
export interface Category {
id: number;
id: string;
name: string;
slug: string;
}
@ -23,7 +23,7 @@ export interface CreatePlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
recipeId?: string;
}
export interface ListItem {
title?: string;
@ -66,7 +66,7 @@ export interface PlanRulesCreate {
tags?: Tag[];
}
export interface Tag {
id: number;
id: string;
name: string;
slug: string;
}
@ -90,13 +90,13 @@ export interface ReadPlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
recipeId?: string;
id: number;
groupId: string;
recipe?: RecipeSummary;
}
export interface RecipeSummary {
id?: number;
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -118,17 +118,17 @@ export interface RecipeSummary {
dateUpdated?: string;
}
export interface RecipeCategory {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTool {
id?: number;
id: string;
name: string;
slug: string;
onHand?: boolean;
@ -147,7 +147,7 @@ export interface IngredientUnit {
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
id: string;
}
export interface CreateIngredientUnit {
name: string;
@ -159,7 +159,7 @@ export interface IngredientFood {
name: string;
description?: string;
labelId?: string;
id: number;
id: string;
label?: MultiPurposeLabelSummary;
}
export interface MultiPurposeLabelSummary {
@ -178,7 +178,7 @@ export interface SavePlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
recipeId?: string;
groupId: string;
}
export interface ShoppingListIn {
@ -197,7 +197,7 @@ export interface UpdatePlanEntry {
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
recipeId?: string;
id: number;
groupId: string;
}

View file

@ -14,7 +14,7 @@ export interface AssignCategories {
}
export interface CategoryBase {
name: string;
id: number;
id: string;
slug: string;
}
export interface AssignTags {
@ -23,7 +23,7 @@ export interface AssignTags {
}
export interface TagBase {
name: string;
id: number;
id: string;
slug: string;
}
export interface BulkActionError {
@ -38,6 +38,15 @@ export interface BulkActionsResponse {
export interface CategoryIn {
name: string;
}
export interface CategoryOut {
name: string;
id: string;
slug: string;
}
export interface CategorySave {
name: string;
groupId: string;
}
export interface CreateIngredientFood {
name: string;
description?: string;
@ -58,12 +67,12 @@ export interface CreateRecipeBulk {
tags?: RecipeTag[];
}
export interface RecipeCategory {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: number;
id: string;
name: string;
slug: string;
}
@ -95,7 +104,7 @@ export interface IngredientFood {
name: string;
description?: string;
labelId?: string;
id: number;
id: string;
label?: MultiPurposeLabelSummary;
}
export interface MultiPurposeLabelSummary {
@ -119,7 +128,7 @@ export interface IngredientUnit {
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
id: string;
}
export interface IngredientsRequest {
parser?: RegisteredParser & string;
@ -149,7 +158,7 @@ export interface RecipeIngredient {
referenceId?: string;
}
export interface Recipe {
id?: number;
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -180,7 +189,7 @@ export interface Recipe {
comments?: RecipeCommentOut[];
}
export interface RecipeTool {
id?: number;
id: string;
name: string;
slug: string;
onHand?: boolean;
@ -210,7 +219,7 @@ export interface RecipeNote {
text: string;
}
export interface RecipeCommentOut {
recipeId: number;
recipeId: string;
text: string;
id: string;
createdAt: string;
@ -225,52 +234,12 @@ export interface UserBase {
}
export interface RecipeCategoryResponse {
name: string;
id: number;
slug: string;
recipes?: Recipe[];
}
export interface RecipeCommentCreate {
recipeId: number;
text: string;
}
export interface RecipeCommentSave {
recipeId: number;
text: string;
userId: string;
}
export interface RecipeCommentUpdate {
id: string;
text: string;
}
export interface RecipeShareToken {
recipeId: number;
expiresAt?: string;
groupId: string;
id: string;
createdAt: string;
recipe: Recipe;
}
export interface RecipeShareTokenCreate {
recipeId: number;
expiresAt?: string;
}
export interface RecipeShareTokenSave {
recipeId: number;
expiresAt?: string;
groupId: string;
}
export interface RecipeShareTokenSummary {
recipeId: number;
expiresAt?: string;
groupId: string;
id: string;
createdAt: string;
}
export interface RecipeSlug {
id: string;
slug: string;
recipes?: RecipeSummary[];
}
export interface RecipeSummary {
id?: number;
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -291,16 +260,56 @@ export interface RecipeSummary {
dateAdded?: string;
dateUpdated?: string;
}
export interface RecipeCommentCreate {
recipeId: string;
text: string;
}
export interface RecipeCommentSave {
recipeId: string;
text: string;
userId: string;
}
export interface RecipeCommentUpdate {
id: string;
text: string;
}
export interface RecipeShareToken {
recipeId: string;
expiresAt?: string;
groupId: string;
id: string;
createdAt: string;
recipe: Recipe;
}
export interface RecipeShareTokenCreate {
recipeId: string;
expiresAt?: string;
}
export interface RecipeShareTokenSave {
recipeId: string;
expiresAt?: string;
groupId: string;
}
export interface RecipeShareTokenSummary {
recipeId: string;
expiresAt?: string;
groupId: string;
id: string;
createdAt: string;
}
export interface RecipeSlug {
slug: string;
}
export interface RecipeTagResponse {
name: string;
id: number;
id: string;
slug: string;
recipes?: Recipe[];
recipes?: RecipeSummary[];
}
export interface RecipeTool1 {
name: string;
onHand?: boolean;
id: number;
id: string;
slug: string;
}
export interface RecipeToolCreate {
@ -310,14 +319,42 @@ export interface RecipeToolCreate {
export interface RecipeToolResponse {
name: string;
onHand?: boolean;
id: number;
id: string;
slug: string;
recipes?: Recipe[];
}
export interface RecipeToolSave {
name: string;
onHand?: boolean;
groupId: string;
}
export interface SaveIngredientFood {
name: string;
description?: string;
labelId?: string;
groupId: string;
}
export interface SaveIngredientUnit {
name: string;
description?: string;
fraction?: boolean;
abbreviation?: string;
groupId: string;
}
export interface SlugResponse {}
export interface TagIn {
name: string;
}
export interface TagOut {
name: string;
groupId: string;
id: string;
slug: string;
}
export interface TagSave {
name: string;
groupId: string;
}
export interface UnitFoodBase {
name: string;
description?: string;

View file

@ -7,7 +7,7 @@
export interface CategoryBase {
name: string;
id: number;
id: string;
slug: string;
}
export interface ChangePassword {
@ -109,7 +109,7 @@ export interface PrivatePasswordResetToken {
user: PrivateUser;
}
export interface RecipeSummary {
id?: number;
id?: string;
userId?: string;
groupId?: string;
name?: string;
@ -131,17 +131,17 @@ export interface RecipeSummary {
dateUpdated?: string;
}
export interface RecipeCategory {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: number;
id: string;
name: string;
slug: string;
}
export interface RecipeTool {
id?: number;
id: string;
name: string;
slug: string;
onHand?: boolean;
@ -160,7 +160,7 @@ export interface IngredientUnit {
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
id: string;
}
export interface CreateIngredientUnit {
name: string;
@ -172,7 +172,7 @@ export interface IngredientFood {
name: string;
description?: string;
labelId?: string;
id: number;
id: string;
label?: MultiPurposeLabelSummary;
}
export interface MultiPurposeLabelSummary {