recipe-manager/src/backend/routes/import.ts

73 lines
2.1 KiB
TypeScript

import { Router } from 'express';
import { z } from 'zod';
import { UrlImportService } from '../services/UrlImportService.js';
import { SchemaOrgRecipeParserService } from '../services/SchemaOrgRecipeParserService.js';
import { HeuristicRecipeParserService } from '../services/HeuristicRecipeParserService.js';
const importUrlSchema = z.object({
url: z.string().url('A valid URL is required'),
});
export function createImportRoutes(): Router {
const router = Router();
const urlImportService = new UrlImportService();
const schemaOrgParser = new SchemaOrgRecipeParserService();
const heuristicParser = new HeuristicRecipeParserService();
/**
* POST /api/import/url
* Fetch an external recipe page and return imported, normalized Recipe (if found)
*/
router.post('/url', async (req, res) => {
try {
const { url } = importUrlSchema.parse(req.body);
const result = await urlImportService.fetchFromUrl(url);
// Try to parse and normalize Recipe from JSON-LD blocks
let draft: any = null;
for (const block of result.json_ld_blocks) {
draft = schemaOrgParser.parseJsonLdBlock(block);
if (draft) break;
}
// Fallback: heuristic HTML parser when Schema.org data is missing/invalid
if (!draft) {
draft = heuristicParser.parseHtml(result.html, result.source_url);
}
res.status(200).json({
success: true,
data: { ...result, draft_recipe: draft },
error: null,
});
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
success: false,
data: null,
error: error.errors,
});
return;
}
if (error instanceof Error) {
const status = error.message.includes('timed out') ? 504 : 400;
res.status(status).json({
success: false,
data: null,
error: error.message,
});
return;
}
res.status(500).json({
success: false,
data: null,
error: 'Internal server error',
});
}
});
return router;
}