feat: implement recipe analysis service and data models
Test Suite / test (24.15.0) (push) Has been cancelled

- Added RecipeAnalysisService to handle recipe ingredient analysis, including methods for checking ingredient availability and calculating quantities.
- Introduced new TypeScript definitions for recipe analysis results, including ingredient status and summary.
- Created corresponding Dart models for recipe analysis, including RecipeIngredientAnalysis, RecipeAnalysisSummary, and RecipeShoppingCandidate.
- Updated Flutter UI to reflect changes in ingredient availability status.
- Fixed color opacity issue in recipe image card.
This commit is contained in:
Nils-Johan Gynther
2026-05-06 07:54:03 +02:00
parent 969dafdbc6
commit 9fe85a719c
23 changed files with 1271 additions and 693 deletions
+176 -175
View File
@@ -17,6 +17,7 @@ export declare class RecipesService {
private readonly logger;
constructor(prisma: PrismaService, aiService: AiService);
private throwRecipeNotFound;
private normalizeIngredientName;
private assertProductsActive;
private findRecipeByIdOrThrow;
private assertAndClaimRecipeOwner;
@@ -73,6 +74,8 @@ export declare class RecipesService {
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -80,54 +83,52 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
})[]>;
findOne(id: number, userId: number): Promise<{
owner: {
@@ -137,6 +138,8 @@ export declare class RecipesService {
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -144,59 +147,59 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
update(id: number, updateRecipeDto: CreateRecipeDto, userId: number): Promise<{
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -204,51 +207,49 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
remove(id: number, userId: number): Promise<void>;
updateImage(id: number, sourceUrl: string, userId: number): Promise<{
@@ -259,6 +260,8 @@ export declare class RecipesService {
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -266,54 +269,52 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
setVisibility(id: number, userId: number, isPublic: boolean): Promise<{
owner: {
@@ -323,6 +324,8 @@ export declare class RecipesService {
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -330,54 +333,52 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
shareWithUser(id: number, ownerId: number, username: string): Promise<{
owner: {
@@ -387,6 +388,8 @@ export declare class RecipesService {
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -394,54 +397,52 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
unshareWithUser(id: number, ownerId: number, username: string): Promise<{
owner: {
@@ -451,6 +452,8 @@ export declare class RecipesService {
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -458,59 +461,59 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
create(createRecipeDto: CreateRecipeDto, userId: number): Promise<{
ingredients: ({
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -518,55 +521,55 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
name: string;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
addIngredient(id: number, ingredient: CreateIngredientDto, userId: number): Promise<{
product: ({
nutrition: {
id: number;
productId: number;
calories: number | null;
protein: number | null;
fat: number | null;
@@ -574,38 +577,36 @@ export declare class RecipesService {
salt: number | null;
sugar: number | null;
fiber: number | null;
id: number;
productId: number;
} | null;
} & {
category: string | null;
status: string;
name: string;
categoryId: number | null;
canonicalName: string | null;
id: number;
normalizedName: string;
isActive: boolean;
deletedAt: Date | null;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
ownerId: number;
status: string;
normalizedName: string;
category: string | null;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
}>;
suggestRecipesFromInventory(userId: number): Promise<{