Convert submodule to regular directory

This commit is contained in:
Nils-Johan Gynther
2026-04-11 16:46:48 +02:00
parent 343416a28d
commit 4189f94e0e
13 changed files with 1781 additions and 1 deletions
Submodule recipe-document-converter deleted from e13c39a757
@@ -0,0 +1,60 @@
/**
* 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: <PDF-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: <PDF-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();
}
}
+113
View File
@@ -0,0 +1,113 @@
(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
}
@@ -0,0 +1,126 @@
# 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
}
@@ -0,0 +1,368 @@
# 🚀 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
@@ -0,0 +1,15 @@
/**
* 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 {}
@@ -0,0 +1,168 @@
/**
* 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);
}
}
}
@@ -0,0 +1,558 @@
# 🔌 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
---
+89
View File
@@ -0,0 +1,89 @@
# 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
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,162 @@
# 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
@@ -0,0 +1,89 @@
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
Submodule recipe-document-converter/recipe-document-converter added at 34f2279eb2