refactor: Simplify Dockerfile by removing recipe-document-converter build stage and update package.json to remove its dependency
This commit is contained in:
+3
-18
@@ -1,21 +1,9 @@
|
||||
# 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: Bygg applikationen
|
||||
# Stage 1: Bygg applikationen
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Kopiera BYGGT recipe-document-converter (med dist/)
|
||||
COPY --from=converter-build /converter /recipe-document-converter
|
||||
|
||||
# Kopiera backend-filer
|
||||
COPY backend/package.json ./
|
||||
COPY backend/prisma ./prisma
|
||||
@@ -23,16 +11,13 @@ COPY backend/src ./src
|
||||
COPY backend/tsconfig.json ./
|
||||
COPY backend/nest-cli.json ./
|
||||
|
||||
# Köra npm install - det kommer att se /recipe-document-converter med dist/ redan byggt
|
||||
# Köra npm install
|
||||
RUN npm install
|
||||
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
# Stage 4: Kör applikationen
|
||||
# Stage 2: Kör applikationen
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"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",
|
||||
|
||||
@@ -3,7 +3,21 @@ 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, ParsedIngredient } from 'recipe-document-converter';
|
||||
|
||||
// Lokala typdefiniitioner (tidigare från recipe-document-converter)
|
||||
interface ParsedIngredient {
|
||||
rawName: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
interface ParsedRecipe {
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
ingredients: ParsedIngredient[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RecipesService {
|
||||
@@ -468,3 +482,140 @@ export class RecipesService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Parser Functions (previously from recipe-document-converter library)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parsar ett recept i Markdown-format och extraherar namn, beskrivning,
|
||||
* instruktioner och ingredienser.
|
||||
*
|
||||
* Förväntat format:
|
||||
* # Receptnamn
|
||||
* Beskrivning (valfritt stycke efter titeln)
|
||||
*
|
||||
* ## Ingredienser
|
||||
* - 400 g kycklingfilé
|
||||
* - 2 dl grädde (eller crème fraiche)
|
||||
*
|
||||
* ## Instruktioner
|
||||
* 1. Stek kycklingen …
|
||||
*/
|
||||
function parseRecipeMarkdown(markdown: string): ParsedRecipe {
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
let name = '';
|
||||
let description = '';
|
||||
let instructions = '';
|
||||
const ingredients: ParsedIngredient[] = [];
|
||||
|
||||
let currentSection: 'none' | 'description' | 'ingredients' | 'instructions' = 'none';
|
||||
const descriptionLines: string[] = [];
|
||||
const instructionLines: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// H1 — receptnamn
|
||||
if (/^#\s+/.test(trimmed) && !trimmed.startsWith('##')) {
|
||||
name = trimmed.replace(/^#\s+/, '').trim();
|
||||
currentSection = 'description';
|
||||
continue;
|
||||
}
|
||||
|
||||
// H2 — sektionsrubriker
|
||||
if (/^##\s+/.test(trimmed)) {
|
||||
const heading = trimmed.replace(/^##\s+/, '').trim().toLowerCase();
|
||||
if (/ingrediens/.test(heading)) {
|
||||
currentSection = 'ingredients';
|
||||
} else if (/instruktion|tillagning|gör så här|steg/.test(heading)) {
|
||||
currentSection = 'instructions';
|
||||
} else {
|
||||
currentSection = 'none';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Samla rader beroende på sektion
|
||||
switch (currentSection) {
|
||||
case 'description':
|
||||
if (trimmed.length > 0) {
|
||||
descriptionLines.push(trimmed);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ingredients':
|
||||
if (/^[-*]\s+/.test(trimmed)) {
|
||||
const ingredientText = trimmed.replace(/^[-*]\s+/, '');
|
||||
ingredients.push(parseIngredientLine(ingredientText));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'instructions':
|
||||
if (trimmed.length > 0) {
|
||||
instructionLines.push(trimmed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
description = descriptionLines.join('\n');
|
||||
instructions = instructionLines.join('\n');
|
||||
|
||||
return { name, description, instructions, ingredients };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsar en ingrediensrad, t.ex.:
|
||||
* "400 g kycklingfilé"
|
||||
* "2 dl grädde (eller crème fraiche)"
|
||||
* "1 kruka basilika"
|
||||
* "salt"
|
||||
*/
|
||||
function parseIngredientLine(text: string): ParsedIngredient {
|
||||
const trimmed = text.trim();
|
||||
|
||||
// Extrahera eventuell parentes-not i slutet
|
||||
let note: string | null = null;
|
||||
let main = trimmed;
|
||||
const parenMatch = trimmed.match(/\(([^)]+)\)\s*$/);
|
||||
if (parenMatch) {
|
||||
note = parenMatch[1].trim();
|
||||
main = trimmed.slice(0, parenMatch.index).trim();
|
||||
}
|
||||
|
||||
// Försök matcha "kvantitet enhet namn" — t.ex. "400 g kycklingfilé" eller "2.5 dl grädde"
|
||||
const fullMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/);
|
||||
if (fullMatch) {
|
||||
return {
|
||||
quantity: parseNumber(fullMatch[1]),
|
||||
unit: fullMatch[2],
|
||||
rawName: fullMatch[3].trim(),
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
// Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg"
|
||||
const noUnitMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(.+)$/);
|
||||
if (noUnitMatch) {
|
||||
return {
|
||||
quantity: parseNumber(noUnitMatch[1]),
|
||||
unit: 'st',
|
||||
rawName: noUnitMatch[2].trim(),
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
// Bara ett namn, ingen kvantitet — t.ex. "salt"
|
||||
return {
|
||||
quantity: 0,
|
||||
unit: '',
|
||||
rawName: main,
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
function parseNumber(s: string): number {
|
||||
return parseFloat(s.replace(',', '.'));
|
||||
}
|
||||
Reference in New Issue
Block a user