Refactor code structure for improved readability and maintainability
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-06 07:37:59 +02:00
parent e4f201ea36
commit 969dafdbc6
273 changed files with 11357 additions and 39 deletions
+6
View File
@@ -0,0 +1,6 @@
export declare class CreateIngredientDto {
productId: number;
quantity: number;
unit: string;
note?: string;
}
+34
View File
@@ -0,0 +1,34 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CreateIngredientDto = void 0;
const class_validator_1 = require("class-validator");
class CreateIngredientDto {
}
exports.CreateIngredientDto = CreateIngredientDto;
__decorate([
(0, class_validator_1.IsNumber)(),
__metadata("design:type", Number)
], CreateIngredientDto.prototype, "productId", void 0);
__decorate([
(0, class_validator_1.IsNumber)(),
__metadata("design:type", Number)
], CreateIngredientDto.prototype, "quantity", void 0);
__decorate([
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateIngredientDto.prototype, "unit", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateIngredientDto.prototype, "note", void 0);
//# sourceMappingURL=create-ingredient.dto.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"create-ingredient.dto.js","sourceRoot":"","sources":["../../../src/recipes/dto/create-ingredient.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAAiE;AAEjE,MAAa,mBAAmB;CAa/B;AAbD,kDAaC;AAXC;IADC,IAAA,0BAAQ,GAAE;;sDACO;AAGlB;IADC,IAAA,0BAAQ,GAAE;;qDACM;AAGjB;IADC,IAAA,0BAAQ,GAAE;;iDACE;AAIb;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;iDACG"}
@@ -0,0 +1,6 @@
export declare class CreateRecipeIngredientDto {
productId: number;
quantity: number;
unit: string;
note?: string;
}
@@ -0,0 +1,35 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CreateRecipeIngredientDto = void 0;
const class_validator_1 = require("class-validator");
class CreateRecipeIngredientDto {
}
exports.CreateRecipeIngredientDto = CreateRecipeIngredientDto;
__decorate([
(0, class_validator_1.IsInt)(),
__metadata("design:type", Number)
], CreateRecipeIngredientDto.prototype, "productId", void 0);
__decorate([
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
__metadata("design:type", Number)
], CreateRecipeIngredientDto.prototype, "quantity", void 0);
__decorate([
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "unit", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "note", void 0);
//# sourceMappingURL=create-recipe-ingredient.dto.js.map
@@ -0,0 +1 @@
{"version":3,"file":"create-recipe-ingredient.dto.js","sourceRoot":"","sources":["../../../src/recipes/dto/create-recipe-ingredient.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA6E;AAE7E,MAAa,yBAAyB;CAcrC;AAdD,8DAcC;AAZC;IADC,IAAA,uBAAK,GAAE;;4DACW;AAInB;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;;2DACW;AAGlB;IADC,IAAA,0BAAQ,GAAE;;uDACG;AAId;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACG"}
+21
View File
@@ -0,0 +1,21 @@
declare class CreateRecipeIngredientDto {
productId?: number;
rawName: string;
rawLine?: string;
quantity?: number;
unit?: string;
note?: string;
matchConfidence?: number;
matchSource?: string;
alternativeProductIds?: number[];
}
export declare class CreateRecipeDto {
name: string;
description?: string;
instructions?: string;
imageUrl?: string;
servings?: number;
isPublic?: boolean;
ingredients: CreateRecipeIngredientDto[];
}
export {};
+104
View File
@@ -0,0 +1,104 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CreateRecipeDto = void 0;
const class_validator_1 = require("class-validator");
const class_transformer_1 = require("class-transformer");
class CreateRecipeIngredientDto {
}
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsInt)(),
__metadata("design:type", Number)
], CreateRecipeIngredientDto.prototype, "productId", void 0);
__decorate([
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "rawName", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "rawLine", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
__metadata("design:type", Number)
], CreateRecipeIngredientDto.prototype, "quantity", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "unit", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "note", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
__metadata("design:type", Number)
], CreateRecipeIngredientDto.prototype, "matchConfidence", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeIngredientDto.prototype, "matchSource", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsArray)(),
(0, class_validator_1.IsInt)({ each: true }),
__metadata("design:type", Array)
], CreateRecipeIngredientDto.prototype, "alternativeProductIds", void 0);
class CreateRecipeDto {
}
exports.CreateRecipeDto = CreateRecipeDto;
__decorate([
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeDto.prototype, "name", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeDto.prototype, "description", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeDto.prototype, "instructions", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], CreateRecipeDto.prototype, "imageUrl", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsInt)(),
(0, class_validator_1.Min)(1),
__metadata("design:type", Number)
], CreateRecipeDto.prototype, "servings", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsBoolean)(),
__metadata("design:type", Boolean)
], CreateRecipeDto.prototype, "isPublic", void 0);
__decorate([
(0, class_validator_1.IsArray)(),
(0, class_validator_1.ArrayMinSize)(1),
(0, class_validator_1.ValidateNested)({ each: true }),
(0, class_transformer_1.Type)(() => CreateRecipeIngredientDto),
__metadata("design:type", Array)
], CreateRecipeDto.prototype, "ingredients", void 0);
//# sourceMappingURL=create-recipe.dto.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"create-recipe.dto.js","sourceRoot":"","sources":["../../../src/recipes/dto/create-recipe.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAUyB;AACzB,yDAAyC;AAEzC,MAAM,yBAAyB;CAsC9B;AAnCC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;;4DACW;AAGnB;IADC,IAAA,0BAAQ,GAAE;;0DACM;AAIjB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;0DACM;AAKjB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;;2DACW;AAIlB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACG;AAId;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACG;AAKd;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;;kEACkB;AAIzB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;8DACU;AAKrB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,yBAAO,GAAE;IACT,IAAA,uBAAK,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;;wEACW;AAGnC,MAAa,eAAe;CA8B3B;AA9BD,0CA8BC;AA5BC;IADC,IAAA,0BAAQ,GAAE;;6CACG;AAId;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;oDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;qDACW;AAItB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;iDACO;AAKlB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;;iDACW;AAIlB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;iDACO;AAMnB;IAJC,IAAA,yBAAO,GAAE;IACT,IAAA,8BAAY,EAAC,CAAC,CAAC;IACf,IAAA,gCAAc,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,yBAAyB,CAAC;;oDACI"}
+3
View File
@@ -0,0 +1,3 @@
export declare class ParseMarkdownDto {
markdown: string;
}
+22
View File
@@ -0,0 +1,22 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseMarkdownDto = void 0;
const class_validator_1 = require("class-validator");
class ParseMarkdownDto {
}
exports.ParseMarkdownDto = ParseMarkdownDto;
__decorate([
(0, class_validator_1.IsString)(),
(0, class_validator_1.MinLength)(1),
__metadata("design:type", String)
], ParseMarkdownDto.prototype, "markdown", void 0);
//# sourceMappingURL=parse-markdown.dto.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"parse-markdown.dto.js","sourceRoot":"","sources":["../../../src/recipes/dto/parse-markdown.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAAsD;AAEtD,MAAa,gBAAgB;CAI5B;AAJD,4CAIC;AADC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,CAAC,CAAC;;kDACK"}
@@ -0,0 +1,3 @@
export declare class SetRecipeVisibilityDto {
isPublic: boolean;
}
+21
View File
@@ -0,0 +1,21 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SetRecipeVisibilityDto = void 0;
const class_validator_1 = require("class-validator");
class SetRecipeVisibilityDto {
}
exports.SetRecipeVisibilityDto = SetRecipeVisibilityDto;
__decorate([
(0, class_validator_1.IsBoolean)(),
__metadata("design:type", Boolean)
], SetRecipeVisibilityDto.prototype, "isPublic", void 0);
//# sourceMappingURL=set-recipe-visibility.dto.js.map
@@ -0,0 +1 @@
{"version":3,"file":"set-recipe-visibility.dto.js","sourceRoot":"","sources":["../../../src/recipes/dto/set-recipe-visibility.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA4C;AAE5C,MAAa,sBAAsB;CAGlC;AAHD,wDAGC;AADC;IADC,IAAA,2BAAS,GAAE;;wDACO"}
+3
View File
@@ -0,0 +1,3 @@
export declare class ShareRecipeDto {
username: string;
}
+22
View File
@@ -0,0 +1,22 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShareRecipeDto = void 0;
const class_validator_1 = require("class-validator");
class ShareRecipeDto {
}
exports.ShareRecipeDto = ShareRecipeDto;
__decorate([
(0, class_validator_1.IsString)(),
(0, class_validator_1.MinLength)(2),
__metadata("design:type", String)
], ShareRecipeDto.prototype, "username", void 0);
//# sourceMappingURL=share-recipe.dto.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"share-recipe.dto.js","sourceRoot":"","sources":["../../../src/recipes/dto/share-recipe.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAAsD;AAEtD,MAAa,cAAc;CAI1B;AAJD,wCAIC;AADC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,CAAC,CAAC;;gDACK"}
+646
View File
@@ -0,0 +1,646 @@
import { RecipesService } from './recipes.service';
import { CreateRecipeDto } from './dto/create-recipe.dto';
import { CreateIngredientDto } from './dto/create-ingredient.dto';
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
import { ShareRecipeDto } from './dto/share-recipe.dto';
import { SetRecipeVisibilityDto } from './dto/set-recipe-visibility.dto';
declare class UpdateImageDto {
sourceUrl: string;
}
export declare class RecipesController {
private readonly recipesService;
constructor(recipesService: RecipesService);
parseMarkdown(dto: ParseMarkdownDto): Promise<{
name: string;
description: string;
instructions: string;
ingredients: {
rawName: string;
rawLine: string;
alternatives: string[];
quantity: number;
unit: string;
note: string | null;
suggestions: {
productId: number;
productName: string;
score: number;
}[];
}[];
}>;
getAiSuggestions(user: {
userId: number;
}): Promise<{
suggestions: import("./recipes.service").AiRecipeSuggestion[];
}>;
findAll(user: {
userId: number;
}): Promise<({
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
})[]>;
getInventoryPreview(id: number, user: {
userId: number;
}): Promise<{
recipe: {
id: number;
name: string;
description: string | null;
};
ingredients: {
ingredientId: any;
productId: any;
productName: any;
requiredQuantity: number;
requiredUnit: any;
note: any;
availableQuantity: number;
availableUnit: any;
matchingInventoryItems: {
id: any;
quantity: any;
unit: any;
location: any;
brand: any;
bestBeforeDate: any;
}[];
otherInventoryItems: {
id: any;
quantity: any;
unit: any;
location: any;
convertedQuantity: number;
canConvert: boolean;
}[];
status: "missing" | "enough" | "unit_mismatch";
fromPantry: boolean;
missingQuantity: number;
}[];
summary: {
totalIngredients: number;
enoughCount: number;
missingCount: number;
unitMismatchCount: number;
canCookExactly: boolean;
pantryCount: number;
};
}>;
findOne(id: number, user: {
userId: number;
}): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
create(createRecipeDto: CreateRecipeDto, user: {
userId: number;
}): Promise<{
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
update(id: number, createRecipeDto: CreateRecipeDto, user: {
userId: number;
}): Promise<{
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
remove(id: number, user: {
userId: number;
}): Promise<void>;
updateImage(id: number, dto: UpdateImageDto, user: {
userId: number;
}): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
addIngredient(id: number, ingredient: CreateIngredientDto, user: {
userId: number;
}): Promise<{
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
}>;
setVisibility(id: number, dto: SetRecipeVisibilityDto, user: {
userId: number;
}): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
shareRecipe(id: number, dto: ShareRecipeDto, user: {
userId: number;
}): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
unshareRecipe(id: number, username: string, user: {
userId: number;
}): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: import("@prisma/client/runtime/library").Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: import("@prisma/client/runtime/library").JsonValue | null;
recipeId: number;
analysisStatus: string | null;
})[];
shares: {
userId: number;
}[];
} & {
isPublic: boolean;
name: string;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
}
export {};
+188
View File
@@ -0,0 +1,188 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecipesController = void 0;
const common_1 = require("@nestjs/common");
const class_validator_1 = require("class-validator");
const recipes_service_1 = require("./recipes.service");
const create_recipe_dto_1 = require("./dto/create-recipe.dto");
const create_ingredient_dto_1 = require("./dto/create-ingredient.dto");
const parse_markdown_dto_1 = require("./dto/parse-markdown.dto");
const current_user_decorator_1 = require("../auth/decorators/current-user.decorator");
const share_recipe_dto_1 = require("./dto/share-recipe.dto");
const set_recipe_visibility_dto_1 = require("./dto/set-recipe-visibility.dto");
class UpdateImageDto {
}
__decorate([
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], UpdateImageDto.prototype, "sourceUrl", void 0);
let RecipesController = class RecipesController {
constructor(recipesService) {
this.recipesService = recipesService;
}
parseMarkdown(dto) {
return this.recipesService.parseMarkdown(dto);
}
getAiSuggestions(user) {
return this.recipesService.suggestRecipesFromInventory(user.userId);
}
findAll(user) {
return this.recipesService.findAll(user.userId);
}
getInventoryPreview(id, user) {
return this.recipesService.getInventoryPreview(id, user.userId);
}
findOne(id, user) {
return this.recipesService.findOne(id, user.userId);
}
async create(createRecipeDto, user) {
return this.recipesService.create(createRecipeDto, user.userId);
}
async update(id, createRecipeDto, user) {
return this.recipesService.update(id, createRecipeDto, user.userId);
}
async remove(id, user) {
return this.recipesService.remove(id, user.userId);
}
async updateImage(id, dto, user) {
return this.recipesService.updateImage(id, dto.sourceUrl, user.userId);
}
async addIngredient(id, ingredient, user) {
return this.recipesService.addIngredient(id, ingredient, user.userId);
}
async setVisibility(id, dto, user) {
return this.recipesService.setVisibility(id, user.userId, dto.isPublic);
}
async shareRecipe(id, dto, user) {
return this.recipesService.shareWithUser(id, user.userId, dto.username.trim());
}
async unshareRecipe(id, username, user) {
return this.recipesService.unshareWithUser(id, user.userId, username.trim());
}
};
exports.RecipesController = RecipesController;
__decorate([
(0, common_1.Post)('parse-markdown'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [parse_markdown_dto_1.ParseMarkdownDto]),
__metadata("design:returntype", void 0)
], RecipesController.prototype, "parseMarkdown", null);
__decorate([
(0, common_1.Get)('ai-suggestions'),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], RecipesController.prototype, "getAiSuggestions", null);
__decorate([
(0, common_1.Get)(),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], RecipesController.prototype, "findAll", null);
__decorate([
(0, common_1.Get)(':id/inventory-preview'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", void 0)
], RecipesController.prototype, "getInventoryPreview", null);
__decorate([
(0, common_1.Get)(':id'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", void 0)
], RecipesController.prototype, "findOne", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Body)()),
__param(1, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [create_recipe_dto_1.CreateRecipeDto, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "create", null);
__decorate([
(0, common_1.Patch)(':id'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, create_recipe_dto_1.CreateRecipeDto, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "update", null);
__decorate([
(0, common_1.Delete)(':id'),
(0, common_1.HttpCode)(204),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "remove", null);
__decorate([
(0, common_1.Post)(':id/image'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, UpdateImageDto, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "updateImage", null);
__decorate([
(0, common_1.Post)(':id/ingredients'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, create_ingredient_dto_1.CreateIngredientDto, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "addIngredient", null);
__decorate([
(0, common_1.Patch)(':id/visibility'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, set_recipe_visibility_dto_1.SetRecipeVisibilityDto, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "setVisibility", null);
__decorate([
(0, common_1.Post)(':id/share'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, share_recipe_dto_1.ShareRecipeDto, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "shareRecipe", null);
__decorate([
(0, common_1.Delete)(':id/share/:username'),
__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
__param(1, (0, common_1.Param)('username')),
__param(2, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, String, Object]),
__metadata("design:returntype", Promise)
], RecipesController.prototype, "unshareRecipe", null);
exports.RecipesController = RecipesController = __decorate([
(0, common_1.Controller)('recipes'),
__metadata("design:paramtypes", [recipes_service_1.RecipesService])
], RecipesController);
//# sourceMappingURL=recipes.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"recipes.controller.js","sourceRoot":"","sources":["../../src/recipes/recipes.controller.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export declare class RecipesModule {
}
+25
View File
@@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecipesModule = void 0;
const common_1 = require("@nestjs/common");
const prisma_module_1 = require("../prisma/prisma.module");
const ai_module_1 = require("../ai/ai.module");
const recipes_controller_1 = require("./recipes.controller");
const recipes_service_1 = require("./recipes.service");
let RecipesModule = class RecipesModule {
};
exports.RecipesModule = RecipesModule;
exports.RecipesModule = RecipesModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule, ai_module_1.AiModule],
controllers: [recipes_controller_1.RecipesController],
providers: [recipes_service_1.RecipesService],
})
], RecipesModule);
//# sourceMappingURL=recipes.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"recipes.module.js","sourceRoot":"","sources":["../../src/recipes/recipes.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,+CAA2C;AAC3C,6DAAyD;AACzD,uDAAmD;AAO5C,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,oBAAQ,CAAC;QACjC,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,CAAC;KAC5B,CAAC;GACW,aAAa,CAAG"}
+632
View File
@@ -0,0 +1,632 @@
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AiService } from '../ai/ai.service';
import { CreateRecipeDto } from './dto/create-recipe.dto';
import { CreateIngredientDto } from './dto/create-ingredient.dto';
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
export interface AiRecipeSuggestion {
name: string;
description: string;
mainIngredients: string[];
missingIngredients: string[];
estimatedTime: string;
}
export declare class RecipesService {
private readonly prisma;
private readonly aiService;
private readonly logger;
constructor(prisma: PrismaService, aiService: AiService);
private throwRecipeNotFound;
private assertProductsActive;
private findRecipeByIdOrThrow;
private assertAndClaimRecipeOwner;
private assertRecipeOwnedByUser;
getInventoryPreview(id: number, userId: number): Promise<{
recipe: {
id: number;
name: string;
description: string | null;
};
ingredients: {
ingredientId: any;
productId: any;
productName: any;
requiredQuantity: number;
requiredUnit: any;
note: any;
availableQuantity: number;
availableUnit: any;
matchingInventoryItems: {
id: any;
quantity: any;
unit: any;
location: any;
brand: any;
bestBeforeDate: any;
}[];
otherInventoryItems: {
id: any;
quantity: any;
unit: any;
location: any;
convertedQuantity: number;
canConvert: boolean;
}[];
status: "missing" | "enough" | "unit_mismatch";
fromPantry: boolean;
missingQuantity: number;
}[];
summary: {
totalIngredients: number;
enoughCount: number;
missingCount: number;
unitMismatchCount: number;
canCookExactly: boolean;
pantryCount: number;
};
}>;
findAll(userId: number): Promise<({
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
})[]>;
findOne(id: number, userId: number): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
update(id: number, updateRecipeDto: CreateRecipeDto, userId: number): Promise<{
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
remove(id: number, userId: number): Promise<void>;
updateImage(id: number, sourceUrl: string, userId: number): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
setVisibility(id: number, userId: number, isPublic: boolean): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
shareWithUser(id: number, ownerId: number, username: string): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
unshareWithUser(id: number, ownerId: number, username: string): Promise<{
owner: {
id: number;
username: string;
} | null;
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
create(createRecipeDto: CreateRecipeDto, userId: number): Promise<{
ingredients: ({
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | 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;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
}>;
addIngredient(id: number, ingredient: CreateIngredientDto, userId: number): Promise<{
product: ({
nutrition: {
calories: number | null;
protein: number | null;
fat: number | null;
carbohydrates: number | null;
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;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number | null;
quantity: Prisma.Decimal | null;
unit: string | null;
rawName: string;
rawLine: string | null;
note: string | null;
matchConfidence: number | null;
matchSource: string | null;
alternativeProductIds: Prisma.JsonValue | null;
recipeId: number;
analysisStatus: string | null;
}>;
suggestRecipesFromInventory(userId: number): Promise<{
suggestions: AiRecipeSuggestion[];
}>;
parseMarkdown(dto: ParseMarkdownDto): Promise<{
name: string;
description: string;
instructions: string;
ingredients: {
rawName: string;
rawLine: string;
alternatives: string[];
quantity: number;
unit: string;
note: string | null;
suggestions: {
productId: number;
productName: string;
score: number;
}[];
}[];
}>;
}
+682
View File
@@ -0,0 +1,682 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var RecipesService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecipesService = void 0;
const common_1 = require("@nestjs/common");
const fs = require("node:fs/promises");
const path = require("node:path");
const prisma_service_1 = require("../prisma/prisma.service");
const ai_service_1 = require("../ai/ai.service");
const download_image_1 = require("../common/utils/download-image");
const recipe_parser_1 = require("../common/utils/recipe-parser");
const units_1 = require("../common/utils/units");
const IMAGE_DEST_DIR = process.env.IMAGE_DEST_DIR || '/app/recipe-images';
let RecipesService = RecipesService_1 = class RecipesService {
constructor(prisma, aiService) {
this.prisma = prisma;
this.aiService = aiService;
this.logger = new common_1.Logger(RecipesService_1.name);
}
throwRecipeNotFound(id) {
throw new common_1.NotFoundException(`Recipe with id ${id} not found`);
}
async assertProductsActive(productIds) {
if (productIds.length === 0)
return;
const activeProducts = await this.prisma.product.findMany({
where: { id: { in: productIds }, isActive: true },
select: { id: true },
});
if (activeProducts.length !== productIds.length) {
const foundIds = new Set(activeProducts.map((p) => p.id));
const missing = productIds.filter((id) => !foundIds.has(id));
throw new common_1.BadRequestException(`En eller flera ingrediensprodukter är inaktiva eller finns inte: ${missing.join(', ')}`);
}
}
async findRecipeByIdOrThrow(id) {
const recipe = await this.prisma.recipe.findUnique({ where: { id } });
if (!recipe) {
this.throwRecipeNotFound(id);
}
return recipe;
}
async assertAndClaimRecipeOwner(recipe, userId) {
if (recipe.ownerId === null) {
await this.prisma.recipe.update({
where: { id: recipe.id },
data: { ownerId: userId },
});
}
else if (recipe.ownerId !== userId) {
this.throwRecipeNotFound(recipe.id);
}
}
assertRecipeOwnedByUser(recipe, userId, id) {
if (recipe.ownerId !== userId) {
this.throwRecipeNotFound(id);
}
}
async getInventoryPreview(id, userId) {
const recipe = await this.prisma.recipe.findFirst({
where: {
id,
OR: [
{ isPublic: true },
{ ownerId: userId },
{ shares: { some: { userId } } },
],
},
include: {
ingredients: {
include: {
product: true,
},
orderBy: {
id: 'asc',
},
},
},
});
if (!recipe) {
throw new common_1.NotFoundException(`Recipe with id ${id} not found`);
}
const pantryItems = await this.prisma.pantryItem.findMany({
where: { userId },
select: { productId: true },
});
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
const ingredientPreviews = await Promise.all(recipe.ingredients.map(async (ingredient) => {
if (!ingredient.productId || !ingredient.product) {
return {
ingredientId: ingredient.id,
productId: null,
productName: ingredient.rawName || 'Okänd ingrediens',
requiredQuantity: Number(ingredient.quantity ?? 0),
requiredUnit: ingredient.unit || '',
note: ingredient.note,
availableQuantity: 0,
availableUnit: ingredient.unit || '',
matchingInventoryItems: [],
otherInventoryItems: [],
status: 'missing',
fromPantry: false,
missingQuantity: Number(ingredient.quantity ?? 0),
};
}
const requiredUnit = (ingredient.unit ?? '').trim();
const requiredQuantity = Number(ingredient.quantity ?? 0);
const coveredByPantry = pantryProductIds.has(ingredient.productId) ||
(Array.isArray(ingredient.alternativeProductIds) &&
ingredient.alternativeProductIds.some((altId) => pantryProductIds.has(altId)));
if (coveredByPantry) {
return {
ingredientId: ingredient.id,
productId: ingredient.productId,
productName: ingredient.product.canonicalName || ingredient.product.name,
requiredQuantity,
requiredUnit,
note: ingredient.note,
availableQuantity: requiredQuantity,
availableUnit: requiredUnit,
matchingInventoryItems: [],
otherInventoryItems: [],
status: 'enough',
fromPantry: true,
missingQuantity: 0,
};
}
const inventoryItems = await this.prisma.inventoryItem.findMany({
where: {
productId: {
in: [
ingredient.productId,
...(Array.isArray(ingredient.alternativeProductIds)
? ingredient.alternativeProductIds
: []),
],
},
},
orderBy: { createdAt: 'desc' },
});
const sameUnitItems = inventoryItems.filter((item) => requiredUnit
? item.unit.trim().toLowerCase() === requiredUnit.toLowerCase()
: true);
const availableSameUnit = sameUnitItems.reduce((sum, item) => sum + Number(item.quantity), 0);
const otherUnitItems = inventoryItems.filter((item) => requiredUnit
? item.unit.trim().toLowerCase() !== requiredUnit.toLowerCase()
: false);
let availableOtherUnit = 0;
for (const item of otherUnitItems) {
try {
const convertedQuantity = (0, units_1.convertUnit)(Number(item.quantity), item.unit, requiredUnit);
availableOtherUnit += convertedQuantity;
}
catch {
}
}
const totalAvailable = availableSameUnit + availableOtherUnit;
let status;
if (totalAvailable >= requiredQuantity) {
status = 'enough';
}
else if (availableSameUnit === 0 && availableOtherUnit > 0) {
status = 'unit_mismatch';
}
else {
status = 'missing';
}
return {
ingredientId: ingredient.id,
productId: ingredient.productId,
productName: ingredient.product.canonicalName || ingredient.product.name,
requiredQuantity,
requiredUnit,
note: ingredient.note,
availableQuantity: totalAvailable,
availableUnit: requiredUnit,
matchingInventoryItems: sameUnitItems.map((item) => ({
id: item.id,
quantity: item.quantity,
unit: item.unit,
location: item.location,
brand: item.brand || null,
bestBeforeDate: item.bestBeforeDate || null,
})),
otherInventoryItems: otherUnitItems.map((item) => {
const canConvertUnits = requiredUnit ? (0, units_1.canConvert)(item.unit, requiredUnit) : false;
let convertedQuantity = 0;
if (canConvertUnits) {
try {
convertedQuantity = (0, units_1.convertUnit)(Number(item.quantity), item.unit, requiredUnit);
}
catch {
convertedQuantity = 0;
}
}
return {
id: item.id,
quantity: item.quantity,
unit: item.unit,
location: item.location,
convertedQuantity: canConvertUnits ? convertedQuantity : 0,
canConvert: canConvertUnits,
};
}),
status,
fromPantry: false,
missingQuantity: status === 'missing' ? Math.max(0, requiredQuantity - totalAvailable) : 0,
};
}));
const summary = {
totalIngredients: ingredientPreviews.length,
enoughCount: ingredientPreviews.filter((i) => i.status === 'enough').length,
missingCount: ingredientPreviews.filter((i) => i.status === 'missing').length,
unitMismatchCount: ingredientPreviews.filter((i) => i.status === 'unit_mismatch').length,
canCookExactly: ingredientPreviews.every((i) => i.status === 'enough'),
pantryCount: ingredientPreviews.filter((i) => i.fromPantry).length,
};
return {
recipe: {
id: recipe.id,
name: recipe.name,
description: recipe.description,
},
ingredients: ingredientPreviews,
summary,
};
}
async findAll(userId) {
return this.prisma.recipe.findMany({
where: {
OR: [
{ isPublic: true },
{ ownerId: userId },
{ shares: { some: { userId } } },
],
},
include: {
ingredients: {
include: {
product: { include: { nutrition: true } },
},
},
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
}
async findOne(id, userId) {
const recipe = await this.prisma.recipe.findFirst({
where: {
id,
OR: [
{ isPublic: true },
{ ownerId: userId },
{ shares: { some: { userId } } },
],
},
include: {
ingredients: {
include: {
product: { include: { nutrition: true } },
},
},
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
if (!recipe) {
throw new common_1.NotFoundException(`Recipe with id ${id} not found`);
}
return recipe;
}
async update(id, updateRecipeDto, userId) {
const existingRecipe = await this.findRecipeByIdOrThrow(id);
await this.assertAndClaimRecipeOwner(existingRecipe, userId);
await this.assertProductsActive(updateRecipeDto.ingredients
.map((i) => i.productId)
.filter((id) => typeof id === 'number'));
const recipe = await this.prisma.$transaction(async (tx) => {
await tx.recipeIngredient.deleteMany({ where: { recipeId: id } });
return tx.recipe.update({
where: { id },
data: {
name: updateRecipeDto.name,
description: updateRecipeDto.description || null,
instructions: updateRecipeDto.instructions || null,
servings: updateRecipeDto.servings ?? null,
...(updateRecipeDto.isPublic !== undefined && { isPublic: updateRecipeDto.isPublic }),
...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
ingredients: {
create: updateRecipeDto.ingredients.map((ingredient) => ({
productId: ingredient.productId ?? null,
rawName: ingredient.rawName,
rawLine: ingredient.rawLine ?? null,
quantity: ingredient.quantity ?? null,
unit: ingredient.unit?.trim() ? ingredient.unit : null,
note: ingredient.note || null,
alternativeProductIds: ingredient.alternativeProductIds ?? [],
matchConfidence: ingredient.matchConfidence ?? null,
matchSource: ingredient.matchSource ?? null,
})),
},
},
include: {
ingredients: {
include: {
product: { include: { nutrition: true } },
},
},
},
});
});
return recipe;
}
async remove(id, userId) {
const existingRecipe = await this.findRecipeByIdOrThrow(id);
await this.assertAndClaimRecipeOwner(existingRecipe, userId);
await this.prisma.recipeIngredient.deleteMany({ where: { recipeId: id } });
await this.prisma.recipe.delete({ where: { id } });
if (existingRecipe.imageUrl?.startsWith('/images/')) {
const filename = path.basename(existingRecipe.imageUrl);
const filePath = path.join(IMAGE_DEST_DIR, filename);
await fs.unlink(filePath).catch(() => {
});
}
}
async updateImage(id, sourceUrl, userId) {
const existingRecipe = await this.findRecipeByIdOrThrow(id);
this.assertRecipeOwnedByUser(existingRecipe, userId, id);
const imageUrl = await (0, download_image_1.downloadAndOptimizeImage)(sourceUrl, IMAGE_DEST_DIR);
return this.prisma.recipe.update({
where: { id },
data: { imageUrl },
include: {
ingredients: { include: { product: { include: { nutrition: true } } } },
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
}
async setVisibility(id, userId, isPublic) {
const existingRecipe = await this.findRecipeByIdOrThrow(id);
this.assertRecipeOwnedByUser(existingRecipe, userId, id);
if (isPublic) {
const owner = await this.prisma.user.findUnique({
where: { id: userId },
select: { canShareRecipes: true },
});
if (!owner?.canShareRecipes) {
throw new common_1.ForbiddenException('Du har inte behörighet att dela recept.');
}
}
return this.prisma.recipe.update({
where: { id },
data: { isPublic },
include: {
ingredients: { include: { product: { include: { nutrition: true } } } },
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
}
async shareWithUser(id, ownerId, username) {
const recipe = await this.findRecipeByIdOrThrow(id);
this.assertRecipeOwnedByUser(recipe, ownerId, id);
const owner = await this.prisma.user.findUnique({
where: { id: ownerId },
select: { canShareRecipes: true },
});
if (!owner?.canShareRecipes) {
throw new common_1.ForbiddenException('Du har inte behörighet att dela recept.');
}
const targetUser = await this.prisma.user.findUnique({
where: { username },
select: { id: true },
});
if (!targetUser) {
throw new common_1.NotFoundException(`User ${username} not found`);
}
if (targetUser.id === ownerId) {
return this.findOne(id, ownerId);
}
await this.prisma.recipeShare.upsert({
where: { recipeId_userId: { recipeId: id, userId: targetUser.id } },
create: { recipeId: id, userId: targetUser.id },
update: {},
});
return this.findOne(id, ownerId);
}
async unshareWithUser(id, ownerId, username) {
const recipe = await this.findRecipeByIdOrThrow(id);
this.assertRecipeOwnedByUser(recipe, ownerId, id);
const targetUser = await this.prisma.user.findUnique({
where: { username },
select: { id: true },
});
if (!targetUser) {
throw new common_1.NotFoundException(`User ${username} not found`);
}
await this.prisma.recipeShare.deleteMany({
where: { recipeId: id, userId: targetUser.id },
});
return this.findOne(id, ownerId);
}
async create(createRecipeDto, userId) {
await this.assertProductsActive(createRecipeDto.ingredients
.map((i) => i.productId)
.filter((id) => typeof id === 'number'));
this.logger.log(`[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`);
let imageUrl = createRecipeDto.imageUrl || null;
let downloadedImagePath = null;
if (imageUrl && imageUrl.startsWith('http')) {
const externalImageUrl = imageUrl;
try {
imageUrl = await (0, download_image_1.downloadAndOptimizeImage)(imageUrl, IMAGE_DEST_DIR);
downloadedImagePath = imageUrl;
}
catch (err) {
console.warn('[RecipesService] Kunde inte ladda ner receptbild:', err);
imageUrl = externalImageUrl;
}
}
this.logger.log(`[create] Final imageUrl persisted to DB: ${imageUrl ?? 'null'}`);
try {
const recipe = await this.prisma.recipe.create({
data: {
name: createRecipeDto.name,
description: createRecipeDto.description || null,
instructions: createRecipeDto.instructions || null,
imageUrl,
servings: createRecipeDto.servings ?? null,
ownerId: userId,
isPublic: false,
ingredients: {
create: createRecipeDto.ingredients.map((ingredient) => ({
productId: ingredient.productId ?? null,
rawName: ingredient.rawName,
rawLine: ingredient.rawLine ?? null,
quantity: ingredient.quantity ?? null,
unit: ingredient.unit?.trim() ? ingredient.unit : null,
note: ingredient.note || null,
alternativeProductIds: ingredient.alternativeProductIds ?? [],
matchConfidence: ingredient.matchConfidence ?? null,
matchSource: ingredient.matchSource ?? null,
})),
},
},
include: {
ingredients: {
include: {
product: { include: { nutrition: true } },
},
},
},
});
return recipe;
}
catch (err) {
if (downloadedImagePath) {
await fs.unlink(path.join(IMAGE_DEST_DIR, path.basename(downloadedImagePath))).catch(() => { });
}
throw err;
}
}
async addIngredient(id, ingredient, userId) {
const recipe = await this.findRecipeByIdOrThrow(id);
await this.assertRecipeOwnedByUser(recipe, userId, id);
await this.assertProductsActive([ingredient.productId]);
return this.prisma.recipeIngredient.create({
data: {
productId: ingredient.productId,
quantity: ingredient.quantity,
unit: ingredient.unit,
note: ingredient.note || null,
recipeId: id,
},
include: {
product: { include: { nutrition: true } },
},
});
}
async suggestRecipesFromInventory(userId) {
const inventoryItems = await this.prisma.inventoryItem.findMany({
include: { product: { select: { canonicalName: true, name: true } } },
orderBy: { bestBeforeDate: 'asc' },
});
const pantryItems = await this.prisma.pantryItem.findMany({
where: { userId },
include: { product: { select: { canonicalName: true, name: true } } },
});
if (inventoryItems.length === 0 && pantryItems.length === 0) {
return { suggestions: [] };
}
const inventoryLines = inventoryItems.map((item) => {
const name = item.product.canonicalName || item.product.name;
return `- ${item.quantity} ${item.unit} ${name}`;
});
const pantryLines = pantryItems.map((item) => {
const name = item.product.canonicalName || item.product.name;
return `- ${name} (stapelvara, alltid tillgänglig)`;
});
const ingredientSummary = [
inventoryLines.length > 0 ? 'Jag har följande i kylen/skafferiet:' : '',
...inventoryLines,
pantryLines.length > 0 ? '\nStapelvaror (alltid tillgängliga):' : '',
...pantryLines,
]
.filter(Boolean)
.join('\n');
const apiKey = process.env.MISTRAL_API_KEY;
if (!apiKey) {
this.logger.warn('MISTRAL_API_KEY saknas — kan inte generera receptförslag');
return { suggestions: [] };
}
const systemPrompt = `Du är en hjälpsam matlagningsassistent för en svensk livsmedelsapp.
Din uppgift är att föreslå recept baserat på vad användaren har hemma.
Regler:
1. Föreslå 3-5 recept som kan lagas med de tillgängliga ingredienserna.
2. Recepten ska vara realistiska och genomförbara.
3. Det är OK om några få vanliga ingredienser saknas (t.ex. salt, olja, kryddor).
4. Svara ENDAST med giltig JSON i detta exakta format:
{
"suggestions": [
{
"name": "Receptnamn",
"description": "Kort beskrivning på 1-2 meningar",
"mainIngredients": ["ingrediens1", "ingrediens2", "ingrediens3"],
"missingIngredients": ["eventuellt saknad ingrediens"],
"estimatedTime": "30 min"
}
]
}`;
const userPrompt = ingredientSummary;
let raw = '';
try {
const response = await fetch('https://api.mistral.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'mistral-small-latest',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
max_tokens: 1500,
temperature: 0.7,
response_format: { type: 'json_object' },
}),
});
if (!response.ok) {
this.logger.error(`Mistral API-fel vid receptförslag: ${response.status}`);
return { suggestions: [] };
}
const data = await response.json();
raw = data.choices?.[0]?.message?.content ?? '{}';
}
catch (err) {
this.logger.error(`Kunde inte nå Mistral för receptförslag: ${err}`);
return { suggestions: [] };
}
try {
const parsed = JSON.parse(raw);
return { suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [] };
}
catch {
this.logger.error(`Kunde inte parsa AI-svar för receptförslag: ${raw}`);
return { suggestions: [] };
}
}
async parseMarkdown(dto) {
const importerUrl = process.env.IMPORTER_SERVICE_URL || 'http://importer-api:3001';
let parsed;
try {
const response = await fetch(`${importerUrl}/api/recipes/parse-markdown`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ markdown: dto.markdown }),
});
if (!response.ok) {
throw new Error(`Importer svarade ${response.status}`);
}
parsed = (await response.json());
}
catch (err) {
this.logger.error(`Kunde inte nå importer-api för parse-markdown: ${err}`);
parsed = (0, recipe_parser_1.parseRecipeMarkdown)(dto.markdown);
}
const allProducts = await this.prisma.product.findMany({
where: { isActive: true },
select: { id: true, name: true, canonicalName: true, normalizedName: true },
});
const normalize = (s) => s.toLowerCase().trim().replace(/[^a-zåäö0-9\s]/gi, '').replace(/\s+/g, ' ');
const levenshtein = (a, b) => {
const m = a.length;
const n = b.length;
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] =
a[i - 1] === b[j - 1]
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
};
const ingredientsWithSuggestions = parsed.ingredients.map((ingredient) => {
const alternatives = ingredient.alternatives?.length > 1
? ingredient.alternatives
: [ingredient.rawName];
const scoreProduct = (query) => allProducts
.map((product) => {
const targetName = normalize(product.canonicalName || product.name);
const targetNormalized = normalize(product.normalizedName);
if (targetNormalized === query || targetName === query) {
return { product, score: 100 };
}
if (targetName.includes(query) || query.includes(targetName)) {
return { product, score: 70 };
}
const dist = levenshtein(query, targetName);
const maxLen = Math.max(query.length, targetName.length);
const similarity = maxLen === 0 ? 100 : Math.round((1 - dist / maxLen) * 100);
return { product, score: similarity };
})
.filter((s) => s.score >= 40)
.sort((a, b) => b.score - a.score)
.slice(0, 5);
const seenIds = new Set();
const scored = alternatives
.flatMap((alt) => scoreProduct(normalize(alt)))
.filter((s) => {
if (seenIds.has(s.product.id))
return false;
seenIds.add(s.product.id);
return true;
})
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map((s) => ({
productId: s.product.id,
productName: s.product.canonicalName || s.product.name,
score: s.score,
}));
return {
rawName: ingredient.rawName,
rawLine: ingredient.rawName,
alternatives: ingredient.alternatives ?? [],
quantity: ingredient.quantity,
unit: ingredient.unit,
note: ingredient.note,
suggestions: scored,
};
});
return {
name: parsed.name,
description: parsed.description,
instructions: parsed.instructions,
ingredients: ingredientsWithSuggestions,
};
}
};
exports.RecipesService = RecipesService;
exports.RecipesService = RecipesService = RecipesService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
ai_service_1.AiService])
], RecipesService);
//# sourceMappingURL=recipes.service.js.map
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export {};
+77
View File
@@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const units_1 = require("../common/utils/units");
const convert = (q, from, to) => (0, units_1.convertUnit)(q, from, to);
const normalize = (u) => (0, units_1.normalizeUnit)(u);
describe('normalizeUnit', () => {
it('normaliserar aliaser', () => {
expect(normalize('tesked')).toBe('tsk');
expect(normalize('matsked')).toBe('msk');
expect(normalize('gram')).toBe('g');
expect(normalize('kilogram')).toBe('kg');
expect(normalize('deciliter')).toBe('dl');
expect(normalize('milliliter')).toBe('ml');
});
it('hanterar gemener och blanksteg', () => {
expect(normalize(' MSK ')).toBe('msk');
expect(normalize('G')).toBe('g');
});
it('returnerar okänd enhet oförändrad', () => {
expect(normalize('kopp')).toBe('kopp');
});
});
describe('convertUnit', () => {
describe('viktkonvertering', () => {
it('konverterar g → kg', () => {
expect(convert(500, 'g', 'kg')).toBeCloseTo(0.5);
});
it('konverterar kg → g', () => {
expect(convert(1.5, 'kg', 'g')).toBeCloseTo(1500);
});
it('returnerar samma värde för identiska enheter', () => {
expect(convert(200, 'g', 'g')).toBe(200);
});
});
describe('volymkonvertering', () => {
it('konverterar dl → ml', () => {
expect(convert(2, 'dl', 'ml')).toBeCloseTo(200);
});
it('konverterar ml → dl', () => {
expect(convert(150, 'ml', 'dl')).toBeCloseTo(1.5);
});
});
describe('portionskonvertering', () => {
it('konverterar msk → tsk (1 msk ≈ 3 tsk)', () => {
expect(convert(2, 'msk', 'tsk')).toBeCloseTo(6);
});
it('konverterar tsk → msk', () => {
expect(convert(3, 'tsk', 'msk')).toBeCloseTo(1);
});
});
describe('normaliserar aliaser vid konvertering', () => {
it('konverterar "gram" → "kg"', () => {
expect(convert(1000, 'gram', 'kg')).toBeCloseTo(1);
});
it('konverterar "matsked" → "tsk"', () => {
expect(convert(1, 'matsked', 'tsk')).toBeCloseTo(3);
});
});
describe('felhantering', () => {
it('kastar fel för inkompatibla enhetstyper', () => {
expect(() => convert(100, 'g', 'dl')).toThrow();
});
it('kastar fel för noll-kvantitet', () => {
expect(() => convert(0, 'g', 'kg')).toThrow();
});
it('kastar fel för negativ kvantitet', () => {
expect(() => convert(-1, 'g', 'kg')).toThrow();
});
it('kastar fel för tom from-enhet', () => {
expect(() => convert(100, '', 'kg')).toThrow();
});
it('kastar fel för okänd enhet', () => {
expect(() => convert(100, 'kopp', 'dl')).toThrow();
});
});
});
//# sourceMappingURL=recipes.service.spec.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"recipes.service.spec.js","sourceRoot":"","sources":["../../src/recipes/recipes.service.spec.ts"],"names":[],"mappings":"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"}