feat(core): add secure recipe APIs, precise scaling and versioned imports
This commit is contained in:
commit
5d71d32e20
31 files changed
+6293
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { XMLBuilder, XMLParser, XMLValidator } from 'fast-xml-parser';
|
||||
import { collectionSchema, type Recipe } from './recipe';
|
||||
export function parseImport(raw: string, format: 'json' | 'xml') {
|
||||
if (Buffer.byteLength(raw, 'utf8') > 2 * 1024 * 1024)
|
||||
throw new Error('Importdatei ist größer als 2 MB.');
|
||||
let value: unknown;
|
||||
if (format === 'json') value = JSON.parse(raw);
|
||||
else {
|
||||
if (/<!DOCTYPE|<!ENTITY/i.test(raw))
|
||||
throw new Error('DOCTYPE und Entities sind nicht erlaubt.');
|
||||
if (XMLValidator.validate(raw) !== true) throw new Error('XML ist nicht wohlgeformt.');
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
parseTagValue: false,
|
||||
processEntities: false,
|
||||
isArray: (_name, path) =>
|
||||
/\.(recipe|ingredient|step|timer|category|tag|ref)$|\.images\.image$/.test(String(path)),
|
||||
});
|
||||
const root = parser.parse(raw).hamham;
|
||||
if (!root) throw new Error('XML benötigt das Wurzelelement hamham.');
|
||||
const num = (x: unknown, fallback = 0) => (x === undefined || x === '' ? fallback : Number(x));
|
||||
const bool = (x: unknown) => x === 'true';
|
||||
const decode = (s: string) =>
|
||||
s
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, '&');
|
||||
const decodeTree = (v: unknown): unknown =>
|
||||
typeof v === 'string'
|
||||
? decode(v)
|
||||
: Array.isArray(v)
|
||||
? v.map(decodeTree)
|
||||
: v && typeof v === 'object'
|
||||
? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, decodeTree(x)]))
|
||||
: v;
|
||||
const recipes = (root.recipes?.recipe ?? []).map((r: Record<string, any>) => ({
|
||||
...r,
|
||||
servings: num(r.servings),
|
||||
prepMinutes: num(r.prepMinutes),
|
||||
cookMinutes: num(r.cookMinutes),
|
||||
restMinutes: num(r.restMinutes),
|
||||
published: bool(r.published),
|
||||
categories: r.categories?.category ?? [],
|
||||
tags: r.tags?.tag ?? [],
|
||||
images: r.images?.image ?? [],
|
||||
ingredients: (r.ingredients?.ingredient ?? []).map((i: Record<string, any>) => ({
|
||||
...i,
|
||||
amount: i.amount === '' || i.amount === undefined ? null : Number(i.amount),
|
||||
optional: bool(i.optional),
|
||||
})),
|
||||
steps: (r.steps?.step ?? []).map((s: Record<string, any>) => ({
|
||||
...s,
|
||||
ingredients: s.ingredients?.ref ?? [],
|
||||
timers: (s.timers?.timer ?? []).map((t: Record<string, any>) => ({
|
||||
...t,
|
||||
seconds: num(t.seconds),
|
||||
})),
|
||||
})),
|
||||
nutrition:
|
||||
r.nutrition && typeof r.nutrition === 'object'
|
||||
? Object.fromEntries(Object.entries(r.nutrition).map(([k, v]) => [k, num(v)]))
|
||||
: null,
|
||||
}));
|
||||
value = { version: Number(root['@_version']), recipes: decodeTree(recipes) };
|
||||
}
|
||||
return collectionSchema.parse(value);
|
||||
}
|
||||
export function exportRecipes(recipes: Recipe[], format: 'json' | 'xml') {
|
||||
if (format === 'json') return JSON.stringify({ version: 1, recipes }, null, 2);
|
||||
const data = recipes.map((r) => ({
|
||||
...r,
|
||||
categories: { category: r.categories },
|
||||
tags: { tag: r.tags },
|
||||
images: { image: r.images },
|
||||
ingredients: { ingredient: r.ingredients.map((i) => ({ ...i, amount: i.amount ?? '' })) },
|
||||
steps: {
|
||||
step: r.steps.map((s) => ({
|
||||
...s,
|
||||
ingredients: { ref: s.ingredients },
|
||||
timers: { timer: s.timers },
|
||||
})),
|
||||
},
|
||||
nutrition: r.nutrition ?? '',
|
||||
}));
|
||||
return (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n' +
|
||||
new XMLBuilder({ ignoreAttributes: false, format: true }).build({
|
||||
hamham: { '@_version': 1, recipes: { recipe: data } },
|
||||
})
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user