New import in version 0.1
This commit is contained in:
+19
-6
@@ -1,19 +1,32 @@
|
||||
# Stage 1: Installera beroenden
|
||||
# Byggas från projektets rot: docker build -f backend/Dockerfile -t recipe-api:local .
|
||||
|
||||
# Stage 1: Bygg recipe-document-converter
|
||||
FROM node:22-alpine AS converter-build
|
||||
WORKDIR /converter
|
||||
COPY recipe-document-converter/package.json ./
|
||||
RUN npm install
|
||||
COPY recipe-document-converter/src ./src
|
||||
COPY recipe-document-converter/tsconfig.json ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Installera backend-beroenden
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY prisma ./prisma
|
||||
# Gör converter tillgänglig för npm:s file:-referens (../recipe-document-converter från /app)
|
||||
COPY --from=converter-build /converter /recipe-document-converter
|
||||
COPY backend/package.json ./
|
||||
COPY backend/prisma ./prisma
|
||||
RUN npm install
|
||||
|
||||
# Stage 2: Bygg applikationen
|
||||
# Stage 3: Bygg applikationen
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
COPY backend/ .
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Kör applikationen
|
||||
# Stage 4: Kör applikationen
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"prisma:deploy": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"recipe-document-converter": "file:../recipe-document-converter",
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class ParseMarkdownDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
markdown!: string;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, ParseIntPipe, Post, Patch } from '@nestjs/common';
|
||||
import { RecipesService } from './recipes.service';
|
||||
import { CreateRecipeDto } from './dto/create-recipe.dto';
|
||||
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
||||
|
||||
@Controller('recipes')
|
||||
export class RecipesController {
|
||||
constructor(private readonly recipesService: RecipesService) {}
|
||||
|
||||
@Post('parse-markdown')
|
||||
parseMarkdown(@Body() dto: ParseMarkdownDto) {
|
||||
return this.recipesService.parseMarkdown(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.recipesService.findAll();
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateRecipeDto } from './dto/create-recipe.dto';
|
||||
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
||||
import { parseRecipeMarkdown } from 'recipe-document-converter';
|
||||
|
||||
@Injectable()
|
||||
export class RecipesService {
|
||||
@@ -384,4 +386,85 @@ export class RecipesService {
|
||||
|
||||
return recipe;
|
||||
}
|
||||
|
||||
async parseMarkdown(dto: ParseMarkdownDto) {
|
||||
const parsed = parseRecipeMarkdown(dto.markdown);
|
||||
|
||||
const allProducts = await this.prisma.product.findMany({
|
||||
where: { isActive: true },
|
||||
select: { id: true, name: true, canonicalName: true, normalizedName: true },
|
||||
});
|
||||
|
||||
// Normalisera en sträng för jämförelse (lowercase, trim, ta bort skiljetecken)
|
||||
const normalize = (s: string) =>
|
||||
s.toLowerCase().trim().replace(/[^a-zåäö0-9\s]/gi, '').replace(/\s+/g, ' ');
|
||||
|
||||
// Enkel Levenshtein-distans
|
||||
const levenshtein = (a: string, b: string): number => {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp: number[][] = 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 query = normalize(ingredient.rawName);
|
||||
|
||||
const scored = allProducts
|
||||
.map((product) => {
|
||||
const targetName = normalize(product.canonicalName || product.name);
|
||||
const targetNormalized = normalize(product.normalizedName);
|
||||
|
||||
// Exakt träff på normalizedName prioriteras
|
||||
if (targetNormalized === query || targetName === query) {
|
||||
return { product, score: 100 };
|
||||
}
|
||||
|
||||
// Delsträng-match
|
||||
if (targetName.includes(query) || query.includes(targetName)) {
|
||||
return { product, score: 70 };
|
||||
}
|
||||
|
||||
// Levenshtein-baserad likhet
|
||||
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)
|
||||
.map((s) => ({
|
||||
productId: s.product.id,
|
||||
productName: s.product.canonicalName || s.product.name,
|
||||
score: s.score,
|
||||
}));
|
||||
|
||||
return {
|
||||
rawName: ingredient.rawName,
|
||||
quantity: ingredient.quantity,
|
||||
unit: ingredient.unit,
|
||||
note: ingredient.note,
|
||||
suggestions: scored,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
instructions: parsed.instructions,
|
||||
ingredients: ingredientsWithSuggestions,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user