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.
This commit is contained in:
@@ -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: <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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule recipe-document-converter/recipe-document-converter deleted from 34f2279eb2
@@ -0,0 +1,2 @@
|
|||||||
|
export { parseRecipeMarkdown } from './parser';
|
||||||
|
export type { ParsedIngredient, ParsedRecipe } from './parser';
|
||||||
@@ -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(',', '.'));
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2021"],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user