From 2330ea938c5604ca0a44245915b54190e9fdb6eb Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Sat, 11 Apr 2026 17:20:53 +0200 Subject: [PATCH] feat: remove import service module and integration guide - Deleted ImportModule and ImportService files as part of the refactor. - Removed the Integration Guide and README documentation for the import service. - Cleaned up Docker Compose files related to the import service. - Added a new parser for recipe markdown format with structured data extraction. - Introduced TypeScript configuration and package.json for the new service structure. --- .../CONTROLLER_EXAMPLE.ts | 60 -- recipe-document-converter/Caddyfile.fixed | 113 ---- .../Caddyfile.production | 126 ---- recipe-document-converter/DEPLOYMENT_GUIDE.md | 368 ------------ .../IMPORT_MODULE_EXAMPLE.ts | 15 - .../INTEGRATION_EXAMPLE.ts | 168 ------ .../INTEGRATION_GUIDE.md | 558 ------------------ recipe-document-converter/README.md | 89 --- .../docker-compose.import-only.yml | 32 - .../docker-compose.production.yml | 162 ----- .../docker-compose.your-setup.yml | 89 --- recipe-document-converter/package.json | 13 + .../recipe-document-converter | 1 - recipe-document-converter/src/index.ts | 2 + recipe-document-converter/src/parser.ts | 146 +++++ recipe-document-converter/tsconfig.json | 13 + 16 files changed, 174 insertions(+), 1781 deletions(-) delete mode 100644 recipe-document-converter/CONTROLLER_EXAMPLE.ts delete mode 100644 recipe-document-converter/Caddyfile.fixed delete mode 100644 recipe-document-converter/Caddyfile.production delete mode 100644 recipe-document-converter/DEPLOYMENT_GUIDE.md delete mode 100644 recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts delete mode 100644 recipe-document-converter/INTEGRATION_EXAMPLE.ts delete mode 100644 recipe-document-converter/INTEGRATION_GUIDE.md delete mode 100644 recipe-document-converter/README.md delete mode 100644 recipe-document-converter/docker-compose.import-only.yml delete mode 100644 recipe-document-converter/docker-compose.production.yml delete mode 100644 recipe-document-converter/docker-compose.your-setup.yml create mode 100644 recipe-document-converter/package.json delete mode 160000 recipe-document-converter/recipe-document-converter create mode 100644 recipe-document-converter/src/index.ts create mode 100644 recipe-document-converter/src/parser.ts create mode 100644 recipe-document-converter/tsconfig.json diff --git a/recipe-document-converter/CONTROLLER_EXAMPLE.ts b/recipe-document-converter/CONTROLLER_EXAMPLE.ts deleted file mode 100644 index 40fe02e4..00000000 --- a/recipe-document-converter/CONTROLLER_EXAMPLE.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * RECIPE CONTROLLER INTEGRATION EXAMPLE - * - * This shows how to add import endpoints to your existing recipe controller. - * Add these methods to backend/src/modules/recipes/recipes.controller.ts - */ - -import { Controller, Post, UseInterceptors, UploadedFile, Get } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { ImportService } from '../import/import.service'; -import { RecipesService } from './recipes.service'; - -@Controller('recipes') -export class RecipesController { - constructor( - private recipesService: RecipesService, - private importService: ImportService, // Add this - ) {} - - // ... existing endpoints ... - - /** - * Import a recipe from PDF without saving - * Useful for preview before saving - * - * POST /recipes/import/pdf - * Content-Type: multipart/form-data - * Body: { file: } - */ - @Post('import/pdf') - @UseInterceptors(FileInterceptor('file')) - async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { - return this.importService.importRecipeFromPDF(file); - } - - /** - * Import a recipe from PDF and save it to database - * - * POST /recipes/import/pdf/save - * Content-Type: multipart/form-data - * Body: { file: } - */ - @Post('import/pdf/save') - @UseInterceptors(FileInterceptor('file')) - async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { - const imported = await this.importService.importRecipeFromPDF(file); - return this.importService.saveImportedRecipe(imported.data); - } - - /** - * Check if import service is available - * Useful for health monitoring - * - * GET /recipes/import/health - */ - @Get('import/health') - async importServiceHealth() { - return this.importService.checkImportServiceHealth(); - } -} diff --git a/recipe-document-converter/Caddyfile.fixed b/recipe-document-converter/Caddyfile.fixed deleted file mode 100644 index e84a0d99..00000000 --- a/recipe-document-converter/Caddyfile.fixed +++ /dev/null @@ -1,113 +0,0 @@ -(common) { - encode gzip zstd - header { - X-Frame-Options "SAMEORIGIN" - X-Content-Type-Options "nosniff" - } -} - -test.gynther.se { - respond "det fungerar" -} - -bazarr.gynther.se { - import common - reverse_proxy http://bazarr:6767 -} - -prowlarr.gynther.se { - import common - reverse_proxy http://prowlarr:9696 -} - -radarr.gynther.se { - import common - reverse_proxy http://radarr:7878 -} - -sonarr.gynther.se { - import common - reverse_proxy http://sonarr:8989 -} - -jellyfin.gynther.se { - reverse_proxy http://jellyfin:8096 -} - -qbittorrent.gynther.se { - import common - reverse_proxy 192.168.50.4:8080 -} - -wetty.gynther.se { - import common - basic_auth { - admin $2a$14$DahHUWD2cKyXJ96sH5VQwuQv1bqmIn0gsdoSaw4mofzfdNY2Y0VsO - } - redir / /wetty - reverse_proxy wetty:3000 -} - -portainer.gynther.se { - reverse_proxy portainer:9000 -} - -gitea.gynther.se { - import common - reverse_proxy 192.168.50.2:3002 -} - -# ============================================ -# RECIPE APP + IMPORT SERVICE -# ============================================ -recept.gynther.se { - import common - - # === IMPORT SERVICE (Document Converter) === - # Dessa endpoints måste komma FÖRST innan backend reglerna! - handle /api/recipes/import* { - reverse_proxy recipe-import-service:3000 - } - - # === RECIPE FRONTEND PROXY ENDPOINTS === - # Next.js API routes - handle /api/inventory-history-proxy { - reverse_proxy recipe-frontend:3000 - } - - handle /api/admin/merge-preview-proxy { - reverse_proxy recipe-frontend:3000 - } - - handle /api/recipe-preview-proxy { - reverse_proxy recipe-frontend:3000 - } - - # === RECIPE BACKEND API ENDPOINTS === - # Backend körs på port 8080 (från docker-compose) - handle /api/products* { - reverse_proxy recipe-api:8080 - } - - handle /api/inventory* { - reverse_proxy recipe-api:8080 - } - - handle /api/recipes* { - reverse_proxy recipe-api:8080 - } - - # === HEALTH CHECKS === - handle /health { - reverse_proxy recipe-api:8080 - } - - # === CATCH ALL === - # Övriga /api/* går till frontend - handle /api/* { - reverse_proxy recipe-frontend:3000 - } - - # Frontend - catch all remaining routes (port 3000) - reverse_proxy /* recipe-frontend:3000 -} diff --git a/recipe-document-converter/Caddyfile.production b/recipe-document-converter/Caddyfile.production deleted file mode 100644 index d2350fda..00000000 --- a/recipe-document-converter/Caddyfile.production +++ /dev/null @@ -1,126 +0,0 @@ -# Production Caddyfile för recipe-app + import-service -# Uppdaterad för import-service integration -# Placera denna fil på: /opt/containers/caddy/conf/Caddyfile - -(common) { - encode gzip zstd - header { - X-Frame-Options "SAMEORIGIN" - X-Content-Type-Options "nosniff" - } -} - -test.gynther.se { - respond "det fungerar" -} - -bazarr.gynther.se { - import common - reverse_proxy http://bazarr:6767 -} - -prowlarr.gynther.se { - import common - reverse_proxy http://prowlarr:9696 -} - -radarr.gynther.se { - import common - reverse_proxy http://radarr:7878 -} - -sonarr.gynther.se { - import common - reverse_proxy http://sonarr:8989 -} - -jellyfin.gynther.se { - reverse_proxy http://jellyfin:8096 -} - -qbittorrent.gynther.se { - import common - reverse_proxy 192.168.50.4:8080 -} - -wetty.gynther.se { - import common - basic_auth { - admin $2a$14$DahHUWD2cKyXJ96sH5VQwuQv1bqmIn0gsdoSaw4mofzfdNY2Y0VsO - } - redir / /wetty - reverse_proxy wetty:3000 -} - -portainer.gynther.se { - reverse_proxy portainer:9000 -} - -gitea.gynther.se { - import common - reverse_proxy 192.168.50.2:3002 -} - -# ============================================ -# Import Service (Document Converter) - Standalone UI -# ============================================ -import.gynther.se { - import common - reverse_proxy recipe-import-service:3000 -} - -# ============================================ -# Recipe App + Import Service Integration -# ============================================ -recept.gynther.se { - import common - - # === IMPORT SERVICE (Document Converter) === - # Dessa endpoints måste komma FÖRST innan backend/frontend reglerna! - # POST /api/recipes/import/pdf - Importera PDF (preview) - # POST /api/recipes/import/pdf/save - Importera och spara - # GET /api/recipes/import/health - Health check - - handle /api/recipes/import* { - reverse_proxy recipe-import-service:3000 - } - - # === RECIPE FRONTEND PROXY ENDPOINTS === - # Proxy-endpoints för Next.js API routes (måste komma FÖRE backend-reglerna!) - handle /api/inventory-history-proxy { - reverse_proxy recipe-frontend:3000 - } - handle /api/admin/merge-preview-proxy { - reverse_proxy recipe-frontend:3000 - } - handle /api/recipe-preview-proxy { - reverse_proxy recipe-frontend:3000 - } - - # === RECIPE BACKEND API === - # Proxy specifika backend-endpoints - handle /api/products* { - reverse_proxy recipe-api:8080 - } - handle /api/inventory* { - reverse_proxy recipe-api:8080 - } - handle /api/recipes* { - reverse_proxy recipe-api:8080 - } - - # === HEALTH CHECKS === - # Health endpoint för backend - handle /health { - reverse_proxy recipe-api:8080 - } - - # === CATCH ALL === - # Övriga /api/* går till Next.js frontend - handle /api/* { - reverse_proxy recipe-frontend:3000 - } - - # Alla andra requests går till frontend (Next.js) - reverse_proxy recipe-frontend:3000 -} diff --git a/recipe-document-converter/DEPLOYMENT_GUIDE.md b/recipe-document-converter/DEPLOYMENT_GUIDE.md deleted file mode 100644 index bd5f9dd3..00000000 --- a/recipe-document-converter/DEPLOYMENT_GUIDE.md +++ /dev/null @@ -1,368 +0,0 @@ -# 🚀 Deployment Guide: Import Service Integration - -Din nuvarande setup: -``` -recept.gynther.se → Caddy Proxy - ├─ recipe-frontend:3000 (Next.js) - ├─ recipe-api:8080 (NestJS Backend) - └─ MariaDB -``` - -Ny setup (med import-service): -``` -recept.gynther.se → Caddy Proxy - ├─ recipe-frontend:3000 (Next.js) - ├─ recipe-api:8080 (NestJS Backend) *← kan anropa import-service* - ├─ recipe-import-service:3000 (NestJS Import) *← NYT* - └─ MariaDB -``` - ---- - -## 📋 Steg-för-steg deployment - -### 1. Uppdatera Caddy configuration - -**Option A: Manuell uppdatering** -```bash -# SSH till server -ssh user@server.se - -# Backup nuvarande Caddyfile -cp /opt/containers/caddy/conf/Caddyfile /opt/containers/caddy/conf/Caddyfile.backup - -# Uppdatera med nya import-service reglerna -# Se Caddyfile.production för den kompletta konfigurationen -``` - -**Option B: Använda versionerad fil** -```bash -# Kopiera Caddyfile.production till servern -scp Caddyfile.production user@server.se:/opt/containers/caddy/conf/Caddyfile - -# Reload Caddy -docker exec caddy-proxy caddy reload -``` - ---- - -### 2. Starta import-service container - -**Om du använder din nuvarande docker-compose:** - -```bash -# SSH till server -ssh user@server.se - -# Navigera till import-service mapp -cd /path/to/recipe-document-converter/recipe-document-converter - -# Build och starta -docker-compose up -d recipe-import-service - -# Eller använd production docker-compose: -docker-compose -f docker-compose.production.yml up -d recipe-import-service -``` - -**Kontrollera att den körs:** -```bash -docker ps | grep import-service -docker logs recipe-import-service - -# Test health endpoint -curl http://localhost:3000/health -``` - ---- - -### 3. Integrera import-service i recipe-app backend - -Lägg till i ditt `recipe-app/backend` projekt: - -**Skapa `src/modules/import/import.service.ts`:** -```typescript -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import axios from 'axios'; -import FormData from 'form-data'; -import * as fs from 'fs'; -import { PrismaService } from '../../prisma/prisma.service'; - -interface RecipeImportDTO { - name: string; - ingredients: Array<{ name: string; quantity?: number; unit?: string }>; - instructions: string[]; - metadata?: { prepTime?: string; servings?: number }; -} - -@Injectable() -export class ImportService { - private readonly IMPORT_SERVICE_URL = process.env.IMPORT_SERVICE_URL || 'http://recipe-import-service:3000'; - - constructor(private prisma: PrismaService) {} - - async importRecipeFromPDF(file: Express.Multer.File) { - try { - if (!file || file.mimetype !== 'application/pdf') { - throw new HttpException('Endast PDF-filer tillåtna', HttpStatus.BAD_REQUEST); - } - - const form = new FormData(); - form.append('file', fs.createReadStream(file.path)); - - const response = await axios.post(`${this.IMPORT_SERVICE_URL}/import/pdf`, form, { - headers: form.getHeaders(), - timeout: 30000, - }); - - if (!response.data.success) { - throw new HttpException(response.data.error, HttpStatus.BAD_REQUEST); - } - - return { success: true, data: response.data.structuredData, metadata: response.data.pdfMetadata }; - } catch (error) { - if (file?.path && fs.existsSync(file.path)) fs.unlinkSync(file.path); - if (error instanceof axios.AxiosError) { - throw new HttpException(`Import misslyckades: ${error.message}`, HttpStatus.SERVICE_UNAVAILABLE); - } - throw error; - } - } - - async saveImportedRecipe(recipeData: RecipeImportDTO) { - try { - const recipe = await this.prisma.recipe.create({ - data: { - name: recipeData.name, - instructions: recipeData.instructions.join('\n'), - prepTime: recipeData.metadata?.prepTime, - servings: recipeData.metadata?.servings, - ingredients: { - create: recipeData.ingredients.map((ing) => ({ - name: ing.name, - quantity: ing.quantity, - unit: ing.unit, - })), - }, - }, - include: { ingredients: true }, - }); - return { success: true, recipe }; - } catch (error) { - throw new HttpException('Kunde inte spara recept', HttpStatus.INTERNAL_SERVER_ERROR); - } - } -} -``` - -**Skapa `src/modules/import/import.module.ts`:** -```typescript -import { Module } from '@nestjs/common'; -import { ImportService } from './import.service'; - -@Module({ - providers: [ImportService], - exports: [ImportService], -}) -export class ImportModule {} -``` - -**Uppdatera `src/modules/recipes/recipes.controller.ts`:** -```typescript -import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { ImportService } from '../import/import.service'; - -@Controller('recipes') -export class RecipesController { - constructor(private importService: ImportService) {} - - @Post('import/pdf') - @UseInterceptors(FileInterceptor('file')) - async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { - return this.importService.importRecipeFromPDF(file); - } - - @Post('import/pdf/save') - @UseInterceptors(FileInterceptor('file')) - async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { - const imported = await this.importService.importRecipeFromPDF(file); - return this.importService.saveImportedRecipe(imported.data); - } -} -``` - -**Uppdatera `src/app.module.ts`:** -```typescript -import { ImportModule } from './modules/import/import.module'; - -@Module({ - imports: [ - // ... existing modules - ImportModule, - ], -}) -export class AppModule {} -``` - ---- - -### 4. Uppdatera miljövariabler - -**`recipe-app/backend/.env` eller docker environment:** -```env -DATABASE_URL=mysql://recipe_user:secure_password@recipe-db:3306/recipe_db -IMPORT_SERVICE_URL=http://recipe-import-service:3000 -``` - ---- - -### 5. Deploy - -**Lokalt (development):** -```bash -# Terminal 1: import-service -cd recipe-document-converter/recipe-document-converter -npm install -npm run start:dev - -# Terminal 2: recipe-app backend -cd recipe-app/backend -npm install -IMPORT_SERVICE_URL=http://localhost:3000 npm run start:dev - -# Terminal 3: recipe-app frontend -cd recipe-app/frontend -npm run dev -``` - -**Production (Docker):** -```bash -# Option A: Starta import-service separat -ssh user@server.se -cd /path/to/recipe-document-converter -docker-compose up -d recipe-import-service - -# Option B: Använd production docker-compose (all-in-one) -docker-compose -f docker-compose.production.yml up -d - -# Reload Caddy -docker exec caddy-proxy caddy reload -``` - ---- - -## 🧪 Testing - -### Test 1: Import-service health -```bash -curl https://recept.gynther.se/api/recipes/import/health -``` - -**Expected Response:** -```json -{ - "status": "ok", - "timestamp": "...", - "service": "recipe-import-service", - "version": "1.0.0" -} -``` - -### Test 2: Import PDF (preview) -```bash -curl -X POST \ - -F "file=@recipe.pdf" \ - https://recept.gynther.se/api/recipes/import/pdf -``` - -### Test 3: Import & Save -```bash -curl -X POST \ - -F "file=@recipe.pdf" \ - https://recept.gynther.se/api/recipes/import/pdf/save -``` - ---- - -## 📊 Routing Overview - -``` -https://recept.gynther.se - ↓ -Caddy Proxy (:443) - ├─ / → recipe-frontend:3000 - ├─ /api/recipes/import/* → recipe-import-service:3000 - ├─ /api/products* → recipe-api:8080 - ├─ /api/inventory* → recipe-api:8080 - ├─ /api/recipes* → recipe-api:8080 (BUT /api/recipes/import/* intercepts here) - └─ /api/* → recipe-frontend:3000 -``` - -**Wichtigt:** Import-endpoints måste komma FÖRE andra /api/recipes* regler för att inte fastna! - ---- - -## 🔍 Troubleshooting - -### Import-service inte tillgänglig -```bash -# Kontrollera container -docker ps | grep import-service - -# Se loggar -docker logs recipe-import-service - -# Kontrollera Caddy routing -docker exec caddy-proxy curl http://recipe-import-service:3000/health - -# Reload Caddy -docker exec caddy-proxy caddy reload -``` - -### 502 Bad Gateway från Caddy -```bash -# Kontrollera att import-service körs -docker logs recipe-import-service - -# Kontrollera nätverksanslutning -docker network inspect recipe-network - -# Verify environment variable -docker inspect recipe-api | grep IMPORT_SERVICE_URL -``` - -### Filuppladdning misslyckas -```bash -# Kontrollera volym permissions -docker exec recipe-import-service ls -la /app/uploads/ - -# Kontrollera filstorlek -ls -lh recipe.pdf - -# Öka MAX_FILE_SIZE om behövs (standard: 50MB) -docker exec recipe-import-service env | grep MAX_FILE_SIZE -``` - ---- - -## ✅ Deployment Checklist - -- [ ] Caddy.production är uppdaterad med import-service reglerna -- [ ] Import-service container är byggd och klar -- [ ] ImportModule är tillagd i recipe-app backend -- [ ] IMPORT_SERVICE_URL är satt i miljövariabler -- [ ] axios är installerat i backend -- [ ] Caddy reloadad -- [ ] Health endpoints testad -- [ ] PDF import testad -- [ ] Loggar monitorade - ---- - -## 🎯 Nästa Steg - -1. **LLM Integration (Mistral)** - För avancerad recepttolkning -2. **Excel/Word support** - Expandera filformat -3. **Caching** - Redis för snabbare import -4. **Error Monitoring** - Lägg till Sentry eller liknande -5. **Rate Limiting** - Skydda import-endpoint diff --git a/recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts b/recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts deleted file mode 100644 index 30bd47b0..00000000 --- a/recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * RECIPE APP MODULE SETUP - * - * Add import service to your recipe app backend. - * File: backend/src/modules/import/import.module.ts - */ - -import { Module } from '@nestjs/common'; -import { ImportService } from './import.service'; - -@Module({ - providers: [ImportService], - exports: [ImportService], -}) -export class ImportModule {} diff --git a/recipe-document-converter/INTEGRATION_EXAMPLE.ts b/recipe-document-converter/INTEGRATION_EXAMPLE.ts deleted file mode 100644 index d30aacce..00000000 --- a/recipe-document-converter/INTEGRATION_EXAMPLE.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * INTEGRATION GUIDE: RECIPE IMPORT SERVICE - * - * This file shows how to integrate recipe-document-converter with recipe-app backend. - * Copy and adapt this code to your recipe-app/backend directory. - * - * Path: backend/src/modules/import/import.service.ts - */ - -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import axios from 'axios'; -import FormData from 'form-data'; -import * as fs from 'fs'; -import { PrismaService } from '../../prisma/prisma.service'; // Adjust path based on your project - -interface RecipeImportDTO { - name: string; - ingredients: Array<{ - name: string; - quantity?: number; - unit?: string; - }>; - instructions: string[]; - metadata?: { - prepTime?: string; - servings?: number; - author?: string; - }; -} - -@Injectable() -export class ImportService { - private readonly IMPORT_SERVICE_URL = process.env.IMPORT_SERVICE_URL || 'http://import-service:3000'; - - constructor(private prisma: PrismaService) {} - - /** - * Upload a PDF file to the import service and extract recipe data - * - * Usage in your recipe controller: - * @Post('import/pdf') - * @UseInterceptors(FileInterceptor('file')) - * async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { - * return this.importService.importRecipeFromPDF(file); - * } - */ - async importRecipeFromPDF(file: Express.Multer.File) { - try { - // Validate file - if (!file) { - throw new HttpException('Ingen fil angiven', HttpStatus.BAD_REQUEST); - } - - if (file.mimetype !== 'application/pdf') { - throw new HttpException('Endast PDF-filer tillåtna', HttpStatus.BAD_REQUEST); - } - - // Call import-service - const form = new FormData(); - form.append('file', fs.createReadStream(file.path)); - - const response = await axios.post(`${this.IMPORT_SERVICE_URL}/import/pdf`, form, { - headers: form.getHeaders(), - timeout: 30000, - }); - - if (!response.data.success) { - throw new HttpException(response.data.error || 'Receptextrahering misslyckades', HttpStatus.BAD_REQUEST); - } - - // Extract and validate recipe data - const recipeData = this.mapImportDataToRecipe(response.data.structuredData); - - return { - success: true, - data: recipeData, - metadata: response.data.pdfMetadata, - }; - } catch (error) { - // Clean up uploaded file - if (file && file.path && fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - - if (error instanceof axios.AxiosError) { - throw new HttpException( - `Receptextrahering misslyckades: ${error.message}`, - HttpStatus.SERVICE_UNAVAILABLE, - ); - } - - throw error; - } - } - - /** - * Save imported recipe to database - * - * Usage in your recipe controller: - * @Post('import/pdf/save') - * @UseInterceptors(FileInterceptor('file')) - * async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { - * const imported = await this.importService.importRecipeFromPDF(file); - * return this.importService.saveImportedRecipe(imported.data); - * } - */ - async saveImportedRecipe(recipeData: RecipeImportDTO) { - try { - // Create recipe with ingredients - const recipe = await this.prisma.recipe.create({ - data: { - name: recipeData.name, - instructions: recipeData.instructions.join('\n'), - prepTime: recipeData.metadata?.prepTime, - servings: recipeData.metadata?.servings, - author: recipeData.metadata?.author, - ingredients: { - create: recipeData.ingredients.map((ing) => ({ - name: ing.name, - quantity: ing.quantity, - unit: ing.unit, - })), - }, - }, - include: { - ingredients: true, - }, - }); - - return { - success: true, - message: 'Recept sparat', - recipe, - }; - } catch (error) { - throw new HttpException( - `Kunde inte spara recept: ${error instanceof Error ? error.message : 'Okänt fel'}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - /** - * Map import service data to recipe format - */ - private mapImportDataToRecipe(importedData: any): RecipeImportDTO { - return { - name: importedData.name || 'Importerat recept', - ingredients: importedData.ingredients || [], - instructions: importedData.instructions || [], - metadata: importedData.metadata || {}, - }; - } - - /** - * Health check for import service (useful for monitoring) - */ - async checkImportServiceHealth() { - try { - const response = await axios.get(`${this.IMPORT_SERVICE_URL}/health`, { - timeout: 5000, - }); - return response.data; - } catch { - throw new HttpException('Import-service är ej tillgänglig', HttpStatus.SERVICE_UNAVAILABLE); - } - } -} diff --git a/recipe-document-converter/INTEGRATION_GUIDE.md b/recipe-document-converter/INTEGRATION_GUIDE.md deleted file mode 100644 index d0216728..00000000 --- a/recipe-document-converter/INTEGRATION_GUIDE.md +++ /dev/null @@ -1,558 +0,0 @@ -# 🔌 Integration Guide: Recipe Import Service - -Denna guide visar hur du integrerar **recipe-document-converter** (import-service) med din befintliga **recipe-app**. - -## 📋 Innehållsförteckning - -1. [Installation & Setup](#installation--setup) -2. [Arkitektur](#arkitektur) -3. [Backend Integration](#backend-integration) -4. [Docker Deployment](#docker-deployment) -5. [API Endpoints](#api-endpoints) -6. [Testing](#testing) -7. [Troubleshooting](#troubleshooting) - ---- - -## Installation & Setup - -### 1. Krav - -- **recipe-app** — Din befintliga receptapp (Next.js frontend + NestJS backend) -- **recipe-document-converter** — Import-service (denna repo) -- **Docker** 24+ och **Docker Compose** -- **Node.js** 22.x - -### 2. Klona och organisera projekten - -```bash -dev/ -├── recipe-app/ # Din befintliga app -│ ├── frontend/ -│ ├── backend/ -│ ├── Dockerfile -│ └── compose.yml -│ -└── recipe-document-converter/ # Import-service - ├── recipe-document-converter/ - ├── Dockerfile - ├── docker-compose.yml - ├── Caddyfile - └── README.md -``` - -### 3. Uppdatera recipe-document-converter paket - -```bash -cd recipe-document-converter/recipe-document-converter - -# TypeScript är redan uppdaterad till 5.4.5 ✅ -# Installera dependencies -npm install -``` - ---- - -## Arkitektur - -### System Diagram - -``` -┌─────────────────────────────────────────────────────────┐ -│ Caddy Reverse Proxy │ -│ (port 80/443) │ -└────────────┬────────────────┬─────────────────┬─────────┘ - │ │ │ - ┌───────▼───┐ ┌──────▼────────┐ ┌──▼─────────┐ - │ Frontend │ │ Backend API │ │ Import │ - │ :4000 │ │ :3001 │ │ :3000 │ - │ │ │ (NestJS) │ │ (NestJS) │ - │ Next.js │ │ │ │ │ - │ 16.2 │ │ Recipes ┐ │ │ PDF │ - │ │ │ Inventory ├─────┼──extract │ - └───────────┘ │ Products │ │ │ structure │ - │ │ │ │ │ - └──────┬────┘ │ └──────┬──────┘ - │ │ │ - ┌──────▼──────────────┐ │ - │ MariaDB 11 │ │ - │ (recipe_db) │ │ - └─────────────────────┘ │ - │ - ┌─────────────▼─┐ - │ Uploads/ │ - │ (Volym) │ - └───────────────┘ -``` - -### Data Flow - -``` -1. Användare laddar upp PDF - ↓ -2. recipe-app frontend → backend POST /recipes/import/pdf - ↓ -3. Backend → FormData → import-service POST /import/pdf - ↓ -4. import-service extraherar → JSON (ingredients, instructions, metadata) - ↓ -5. Backend validerar + sparar i Prisma/MariaDB - ↓ -6. Frontend visar importerat recept -``` - ---- - -## Backend Integration - -### Steg 1: Uppdatera recipe-app backend - -```bash -cd recipe-app/backend - -# Installera axios om det saknas -npm install axios - -# Om du använder FormData -npm install form-data -``` - -### Steg 2: Skapa import-modul - -Skapa `backend/src/modules/import/import.service.ts`: - -```typescript -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import axios from 'axios'; -import FormData from 'form-data'; -import * as fs from 'fs'; -import { PrismaService } from '../../prisma/prisma.service'; - -interface RecipeImportDTO { - name: string; - ingredients: Array<{ - name: string; - quantity?: number; - unit?: string; - }>; - instructions: string[]; - metadata?: { - prepTime?: string; - servings?: number; - }; -} - -@Injectable() -export class ImportService { - private readonly IMPORT_SERVICE_URL = process.env.IMPORT_SERVICE_URL || 'http://import-service:3000'; - - constructor(private prisma: PrismaService) {} - - async importRecipeFromPDF(file: Express.Multer.File) { - try { - if (!file) { - throw new HttpException('Ingen fil angiven', HttpStatus.BAD_REQUEST); - } - - if (file.mimetype !== 'application/pdf') { - throw new HttpException('Endast PDF-filer tillåtna', HttpStatus.BAD_REQUEST); - } - - // Call import-service - const form = new FormData(); - form.append('file', fs.createReadStream(file.path)); - - const response = await axios.post(`${this.IMPORT_SERVICE_URL}/import/pdf`, form, { - headers: form.getHeaders(), - timeout: 30000, - }); - - if (!response.data.success) { - throw new HttpException(response.data.error, HttpStatus.BAD_REQUEST); - } - - const recipeData = this.mapImportDataToRecipe(response.data.structuredData); - - return { - success: true, - data: recipeData, - metadata: response.data.pdfMetadata, - }; - } catch (error) { - if (file?.path && fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - - if (error instanceof axios.AxiosError) { - throw new HttpException( - `Import misslyckades: ${error.message}`, - HttpStatus.SERVICE_UNAVAILABLE, - ); - } - - throw error; - } - } - - async saveImportedRecipe(recipeData: RecipeImportDTO) { - try { - const recipe = await this.prisma.recipe.create({ - data: { - name: recipeData.name, - instructions: recipeData.instructions.join('\n'), - prepTime: recipeData.metadata?.prepTime, - servings: recipeData.metadata?.servings, - ingredients: { - create: recipeData.ingredients.map((ing) => ({ - name: ing.name, - quantity: ing.quantity, - unit: ing.unit, - })), - }, - }, - include: { ingredients: true }, - }); - - return { success: true, message: 'Recept sparat', recipe }; - } catch (error) { - throw new HttpException( - `Kunde inte spara recept: ${error instanceof Error ? error.message : 'Okänt fel'}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - private mapImportDataToRecipe(importedData: any): RecipeImportDTO { - return { - name: importedData.name || 'Importerat recept', - ingredients: importedData.ingredients || [], - instructions: importedData.instructions || [], - metadata: importedData.metadata || {}, - }; - } -} -``` - -### Steg 3: Skapa import-modul - -Skapa `backend/src/modules/import/import.module.ts`: - -```typescript -import { Module } from '@nestjs/common'; -import { ImportService } from './import.service'; - -@Module({ - providers: [ImportService], - exports: [ImportService], -}) -export class ImportModule {} -``` - -### Steg 4: Lägg till endpoints i recipes controller - -Uppdatera `backend/src/modules/recipes/recipes.controller.ts`: - -```typescript -import { Controller, Post, Get, UseInterceptors, UploadedFile } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { ImportService } from '../import/import.service'; - -@Controller('recipes') -export class RecipesController { - constructor(private importService: ImportService) {} - - /** - * Import recipe from PDF without saving - * POST /recipes/import/pdf - */ - @Post('import/pdf') - @UseInterceptors(FileInterceptor('file')) - async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { - return this.importService.importRecipeFromPDF(file); - } - - /** - * Import and save recipe - * POST /recipes/import/pdf/save - */ - @Post('import/pdf/save') - @UseInterceptors(FileInterceptor('file')) - async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { - const imported = await this.importService.importRecipeFromPDF(file); - return this.importService.saveImportedRecipe(imported.data); - } - - /** - * Check import service health - * GET /recipes/import/health - */ - @Get('import/health') - async importServiceHealth() { - return this.importService.checkImportServiceHealth(); - } -} -``` - -### Steg 5: Registrera import-modul - -Uppdatera `backend/src/app.module.ts`: - -```typescript -import { ImportModule } from './modules/import/import.module'; - -@Module({ - imports: [ - // ... existing modules - ImportModule, - ], -}) -export class AppModule {} -``` - ---- - -## Docker Deployment - -### Option 1: Lokal utveckling (utan Docker) - -```bash -# Terminal 1: Start import-service -cd recipe-document-converter/recipe-document-converter -npm install -npm run start:dev - -# Terminal 2: Start recipe-app backend -cd recipe-app/backend -npm install -IMPORT_SERVICE_URL=http://localhost:3000 npm run start:dev - -# Terminal 3: Start recipe-app frontend -cd recipe-app/frontend -npm install -npm run dev -``` - -**Access:** -- Frontend: http://localhost:3000 (Next.js default) -- Backend: http://localhost:3001/api -- Import: http://localhost:3000/import - ---- - -### Option 2: Docker Compose (Full Stack) - -**Förutsättningar:** -- `recipe-app/` ligger bredvid `recipe-document-converter/` -- recipe-app har `Dockerfile` i root - -**Starta all-in-one:** - -```bash -cd recipe-document-converter/recipe-document-converter - -# Bygg och starta alla tjänster -docker-compose up -d - -# Eller med bygge -docker-compose up -d --build -``` - -**Tjänster:** -``` -- Frontend: http://localhost (via Caddy) -- Backend API: http://localhost/api -- Import Service: http://localhost/import -- Health: http://localhost/health -``` - -**Se loggar:** -```bash -docker-compose logs -f recipe-app-backend -docker-compose logs -f import-service -docker-compose logs -f recipe-db -docker-compose logs -f caddy -``` - -**Stoppa allt:** -```bash -docker-compose down -``` - ---- - -## API Endpoints - -### Import Service Endpoints - -| Method | Endpoint | Beskrivning | Auth | -|--------|----------|-------------|------| -| `GET` | `/health` | Hälsokontroll | Nej | -| `POST` | `/import/pdf` | Importera PDF | Nej | - -### Recipe App Backend Integration Endpoints - -| Method | Endpoint | Beskrivning | -|--------|----------|-------------| -| `POST` | `/api/recipes/import/pdf` | Importera PDF (preview) | -| `POST` | `/api/recipes/import/pdf/save` | Importera och spara | -| `GET` | `/api/recipes/import/health` | Health check | - ---- - -## Testing - -### 1. Test import-service isolerat - -```bash -curl -X POST \ - -F "file=@recipe.pdf" \ - http://localhost:3000/import/pdf -``` - -**Response:** -```json -{ - "success": true, - "rawText": "...", - "pdfMetadata": { - "fileName": "recipe.pdf", - "pages": 1, - "author": "Unknown" - }, - "structuredData": { - "name": "Kycklingcurry", - "ingredients": [...], - "instructions": [...] - } -} -``` - -### 2. Test recipe-app integration - -```bash -curl -X POST \ - -F "file=@recipe.pdf" \ - http://localhost:3001/api/recipes/import/pdf -``` - -### 3. Test health checks - -```bash -# Import service -curl http://localhost:3000/health - -# Recipe app backend -curl http://localhost:3001/health - -# Import from backend -curl http://localhost:3001/api/recipes/import/health -``` - -### 4. Test full end-to-end (Docker) - -```bash -# Start services -docker-compose up -d - -# Wait for services to be healthy -sleep 10 - -# Check all services -curl http://localhost/health -curl http://localhost/api/health -curl http://localhost/import/health - -# Import recipe -curl -X POST \ - -F "file=@recipe.pdf" \ - http://localhost/api/recipes/import/pdf/save -``` - ---- - -## Troubleshooting - -### Import Service är inte tillgänglig - -```bash -# Kontrollera om den körs -docker ps | grep import-service - -# Se loggar -docker logs recipe-import-service - -# Kontrollera hälsa -curl http://localhost:3000/health -``` - -### Docker nätverk-error - -```bash -# Reinitialize docker-compose -docker-compose down -docker system prune -a -docker-compose up --build -``` - -### Filuppladdning misslyckas - -```bash -# Kontrollera filstorlek (default 50MB) -ls -lh recipe.pdf - -# Kontrollera uploads-mapp permissions -docker exec recipe-import-service ls -la /app/uploads/ -``` - -### TypeScript fel - -```bash -# Uppdatera dependencies -npm install - -# Clear cache -npm cache clean --force - -# Rebuild -npm run build -``` - -### Databas-anslutning misslyckad - -```bash -# Kontrollera MariaDB -docker logs recipe-db - -# Kontrollera URL -echo $DATABASE_URL - -# Test direkten -docker exec recipe-db mysql -urecipe_user -psecure_password -e "USE recipe_db; SHOW TABLES;" -``` - ---- - -## Production Checklist - -- [ ] TypeScript compilerar utan fel -- [ ] Alla miljövariabler är satta -- [ ] Database-backup är konfigurerat -- [ ] Import-service är tillgänglig från backend -- [ ] Caddy certifikater är satta upp -- [ ] Log rotation är konfigurerat -- [ ] Health endpoints fungerar -- [ ] File uploads permissions är rätt -- [ ] Rate limiting är konfigurerat (if needed) -- [ ] Monitoring setup är på plats - ---- - -## Nästa steg - -1. **LLM Integration** — Lägg till Mistral för avancerad strukturering -2. **Excel/Word support** — Expandera till fler filformat -3. **OCR** — Lägg till stöd för skannade dokument -4. **Caching** — Implementera Redis för snabbare import -5. **Authentication** — Lägg till auth om behövs - ---- diff --git a/recipe-document-converter/README.md b/recipe-document-converter/README.md deleted file mode 100644 index 01104002..00000000 --- a/recipe-document-converter/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# recipe-document-converter - -🍳 **A Docker-based import service for converting recipe documents (PDF, Word, Excel, Images) into structured JSON data.** - -## 🎯 Project Overview - -This repository contains a **microservice architecture** designed to: - -1. **Extract** recipe content from multiple document formats -2. **Structure** unorganized data using LLM (Mistral) for intelligent parsing -3. **Integrate** seamlessly with recipe applications via REST API -4. **Scale** independently using Docker containerization - -## 📦 Structure - -``` -recipe-document-converter/ -├── recipe-document-converter/ # Main import service (this is the actual service) -│ ├── src/ # TypeScript source code -│ ├── Dockerfile # Container definition -│ ├── docker-compose.yml # Multi-service orchestration -│ ├── package.json # Dependencies -│ └── README.md # Service documentation -└── README.md # This file -``` - -## 🚀 Quick Start - -### Using Docker Compose (Recommended) - -```bash -cd recipe-document-converter - -# Start the service -docker-compose up -d - -# Test the service -curl http://localhost:3000/health -``` - -### Local Development - -```bash -cd recipe-document-converter - -# Install dependencies -npm install - -# Start development server -npm run start:dev -``` - -## 📖 API Endpoints - -| Method | Endpoint | Description | -|--------|---------------|--------------------------------| -| `GET` | `/health` | Health check | -| `POST` | `/import/pdf` | Import and extract PDF recipe | - -## 📚 Full Documentation - -See [recipe-document-converter/README.md](recipe-document-converter/README.md) for complete documentation. - -## 🔮 Planned Features - -- [x] PDF extraction -- [x] Basic recipe structuring -- [ ] Mistral LLM integration -- [ ] Excel support -- [ ] Word support -- [ ] Image OCR support -- [ ] Web scraping - -## 🛠️ Tech Stack - -- **NestJS** — Node.js framework -- **TypeScript** — Type safety -- **Docker** — Containerization -- **pdf-parse** — PDF extraction -- **Zod** — Schema validation (coming soon) -- **Mistral AI** — LLM integration (coming soon) - -## 🤝 Contributing - -Contributions welcome! Open an issue or submit a PR. - -## 📄 License - -MIT diff --git a/recipe-document-converter/docker-compose.import-only.yml b/recipe-document-converter/docker-compose.import-only.yml deleted file mode 100644 index f52cf691..00000000 --- a/recipe-document-converter/docker-compose.import-only.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: '3.8' - -services: - recipe-import-service: - build: - context: ./recipe-document-converter - dockerfile: Dockerfile - image: recipe-import-service:local - container_name: recipe-import-service - restart: unless-stopped - environment: - NODE_ENV: "production" - PORT: "3000" - LOG_LEVEL: "info" - MAX_FILE_SIZE: "50000000" - volumes: - - recipe_imports:/app/uploads - networks: - - recipe-internal - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - -volumes: - recipe_imports: - -networks: - recipe-internal: - driver: bridge diff --git a/recipe-document-converter/docker-compose.production.yml b/recipe-document-converter/docker-compose.production.yml deleted file mode 100644 index 4cf7ae9e..00000000 --- a/recipe-document-converter/docker-compose.production.yml +++ /dev/null @@ -1,162 +0,0 @@ -# Production Docker Compose för recipe-app + import-service -# -# Denna konfiguration: -# - Körs med diagram som visat i Caddyfile.production -# - Integrerar recipe-app (frontend + backend) + import-service -# - Använder MariaDB databas -# - Caddy som reverse proxy -# -# Usage: -# docker-compose -f docker-compose.production.yml up -d -# -# Anpassningar: -# - Uppdatera MYSQL_ROOT_PASSWORD och MYSQL_PASSWORD -# - Uppdatera DATABASE_URL för backend -# - Säkerställ att alla services kan nå varandra via docker-network - -version: '3.8' - -services: - # ============================================ - # Frontend: Next.js 16.2 - # ============================================ - recipe-frontend: - build: - context: ./recipe-app/frontend - dockerfile: Dockerfile - container_name: recipe-frontend - environment: - - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://recept.gynther.se/api - networks: - - recipe-network - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # ============================================ - # Backend: NestJS 10.3 + Prisma - # ============================================ - recipe-api: - build: - context: ./recipe-app/backend - dockerfile: Dockerfile - container_name: recipe-api - depends_on: - recipe-db: - condition: service_healthy - recipe-import-service: - condition: service_healthy - environment: - - NODE_ENV=production - - PORT=8080 - - DATABASE_URL=mysql://recipe_user:${DB_PASSWORD:-secure_password}@recipe-db:3306/recipe_db?schema=public - - IMPORT_SERVICE_URL=http://recipe-import-service:3000 - - LOG_LEVEL=info - networks: - - recipe-network - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # ============================================ - # Database: MariaDB 11 - # ============================================ - recipe-db: - image: mariadb:11 - container_name: recipe-db - environment: - - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-root_password} - - MYSQL_DATABASE=recipe_db - - MYSQL_USER=recipe_user - - MYSQL_PASSWORD=${DB_PASSWORD:-secure_password} - - MYSQL_INITDB_SKIP_TZINFO=1 - volumes: - - recipe-db-data:/var/lib/mysql - networks: - - recipe-network - restart: unless-stopped - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 10s - timeout: 5s - retries: 5 - - # ============================================ - # Import Service: Document Converter (NestJS) - # ============================================ - recipe-import-service: - build: - context: ./recipe-document-converter/recipe-document-converter - dockerfile: Dockerfile - container_name: recipe-import-service - environment: - - NODE_ENV=production - - PORT=3000 - - LOG_LEVEL=info - - MAX_FILE_SIZE=50000000 - volumes: - - recipe-imports-data:/app/uploads - networks: - - recipe-network - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # ============================================ - # Reverse Proxy: Caddy 2.x - # ============================================ - caddy: - image: caddy:2.7 - container_name: caddy-proxy - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile.production:/etc/caddy/Caddyfile:ro - - caddy-data:/data - - caddy-config:/config - networks: - - recipe-network - depends_on: - - recipe-frontend - - recipe-api - - recipe-import-service - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - -# ============================================ -# Networks -# ============================================ -networks: - recipe-network: - driver: bridge - -# ============================================ -# Volumes -# ============================================ -volumes: - recipe-db-data: - driver: local - recipe-imports-data: - driver: local - caddy-data: - driver: local - caddy-config: - driver: local diff --git a/recipe-document-converter/docker-compose.your-setup.yml b/recipe-document-converter/docker-compose.your-setup.yml deleted file mode 100644 index 5ea82598..00000000 --- a/recipe-document-converter/docker-compose.your-setup.yml +++ /dev/null @@ -1,89 +0,0 @@ -services: - recipe-frontend: - image: recipe-frontend:local - container_name: recipe-frontend - restart: unless-stopped - environment: - NODE_ENV: "production" - HOSTNAME: "0.0.0.0" - PORT: "3000" - NEXT_PUBLIC_APP_URL: "https://recept.gynther.se" - NEXT_PUBLIC_API_URL: "https://api.recept.gynther.se" - NEXT_PUBLIC_API_URL_INTERNAL: "http://recipe-api:8080" - networks: - - proxy - - recipe-internal - - recipe-api: - image: recipe-api:local - container_name: recipe-api - restart: unless-stopped - depends_on: - recipe-db: - condition: service_healthy - recipe-import-service: - condition: service_healthy - environment: - NODE_ENV: "production" - DATABASE_URL: "mysql://recipe_user:Imminent-Umpire-Undertook8-Crunchy@recipe-db:3306/recipe_app" - # New: Import service URL for backend to call - IMPORT_SERVICE_URL: "http://recipe-import-service:3000" - networks: - - proxy - - recipe-internal - - recipe-db: - image: mariadb:11 - container_name: recipe-db - restart: unless-stopped - environment: - MARIADB_ROOT_PASSWORD: "Encrust6-Deserve-Stricken-Spectacle" - MARIADB_DATABASE: "recipe_app" - MARIADB_USER: "recipe_user" - MARIADB_PASSWORD: "Imminent-Umpire-Undertook8-Crunchy" - volumes: - - recipe_db_data:/var/lib/mysql - command: - - --character-set-server=utf8mb4 - - --collation-server=utf8mb4_unicode_ci - networks: - - recipe-internal - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 10s - timeout: 5s - retries: 5 - - # === NEW: Import Service (Document Converter) === - recipe-import-service: - build: - context: ./recipe-document-converter/recipe-document-converter - dockerfile: Dockerfile - image: recipe-import-service:local - container_name: recipe-import-service - restart: unless-stopped - environment: - NODE_ENV: "production" - PORT: "3000" - LOG_LEVEL: "info" - MAX_FILE_SIZE: "50000000" - volumes: - - recipe_imports:/app/uploads - networks: - - recipe-internal # Only internal communication with recipe-api - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - -volumes: - recipe_db_data: - recipe_imports: - -networks: - proxy: - external: true - recipe-internal: - driver: bridge diff --git a/recipe-document-converter/package.json b/recipe-document-converter/package.json new file mode 100644 index 00000000..242f1357 --- /dev/null +++ b/recipe-document-converter/package.json @@ -0,0 +1,13 @@ +{ + "name": "recipe-document-converter", + "version": "1.0.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc" + }, + "devDependencies": { + "typescript": "^5.4.5" + } +} diff --git a/recipe-document-converter/recipe-document-converter b/recipe-document-converter/recipe-document-converter deleted file mode 160000 index 34f2279e..00000000 --- a/recipe-document-converter/recipe-document-converter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 34f2279eb2bfa89356bb20193f2c909a7d1d4bad diff --git a/recipe-document-converter/src/index.ts b/recipe-document-converter/src/index.ts new file mode 100644 index 00000000..e3c710a4 --- /dev/null +++ b/recipe-document-converter/src/index.ts @@ -0,0 +1,2 @@ +export { parseRecipeMarkdown } from './parser'; +export type { ParsedIngredient, ParsedRecipe } from './parser'; diff --git a/recipe-document-converter/src/parser.ts b/recipe-document-converter/src/parser.ts new file mode 100644 index 00000000..dde9fe29 --- /dev/null +++ b/recipe-document-converter/src/parser.ts @@ -0,0 +1,146 @@ +export interface ParsedIngredient { + rawName: string; + quantity: number; + unit: string; + note: string | null; +} + +export interface ParsedRecipe { + name: string; + description: string; + instructions: string; + ingredients: ParsedIngredient[]; +} + +/** + * 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 … + */ +export 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(',', '.')); +} diff --git a/recipe-document-converter/tsconfig.json b/recipe-document-converter/tsconfig.json new file mode 100644 index 00000000..522d4031 --- /dev/null +++ b/recipe-document-converter/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "commonjs", + "lib": ["ES2021"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +}