Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ALLERGENS,
|
||||
COOKING_METHODS,
|
||||
CUISINES,
|
||||
EQUIPMENT,
|
||||
FEEDBACK_TAGS,
|
||||
MEAL_TYPES,
|
||||
RECIPE_DIFFICULTIES,
|
||||
RECIPE_TAGS,
|
||||
} from "@app/shared-types";
|
||||
import { quantitySchema, unitSchema, uuidSchema } from "./common.js";
|
||||
|
||||
export const recipeQuerySchema = z.object({
|
||||
search: z.string().max(120).optional(),
|
||||
cuisine: z.enum(CUISINES).optional(),
|
||||
mealType: z.enum(MEAL_TYPES).optional(),
|
||||
tags: z
|
||||
.union([z.enum(RECIPE_TAGS), z.array(z.enum(RECIPE_TAGS))])
|
||||
.transform((v) => (Array.isArray(v) ? v : [v]))
|
||||
.optional(),
|
||||
method: z.enum(COOKING_METHODS).optional(),
|
||||
maxTotalMinutes: z.coerce.number().int().min(1).max(600).optional(),
|
||||
maxKcalPerPortion: z.coerce.number().int().min(50).max(5000).optional(),
|
||||
minProteinPerPortion: z.coerce.number().int().min(0).max(300).optional(),
|
||||
maxCostMinorPerPortion: z.coerce.number().int().min(0).max(100_000).optional(),
|
||||
difficulty: z.enum(RECIPE_DIFFICULTIES).optional(),
|
||||
excludeAllergens: z
|
||||
.union([z.enum(ALLERGENS), z.array(z.enum(ALLERGENS))])
|
||||
.transform((v) => (Array.isArray(v) ? v : [v]))
|
||||
.optional(),
|
||||
creatorUserId: uuidSchema.optional(),
|
||||
sort: z.enum(["relevance", "rating", "cooked", "newest", "time", "cost"]).default("relevance"),
|
||||
limit: z.coerce.number().int().min(1).max(50).default(20),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
});
|
||||
export type RecipeQuery = z.infer<typeof recipeQuerySchema>;
|
||||
|
||||
const recipeIngredientInputSchema = z.object({
|
||||
canonicalIngredientId: z.string().min(1).max(80),
|
||||
displayNameSv: z.string().min(1).max(120),
|
||||
quantity: quantitySchema,
|
||||
unit: unitSchema,
|
||||
note: z.string().max(120).optional(),
|
||||
optional: z.boolean().default(false),
|
||||
groupName: z.string().max(60).optional(),
|
||||
});
|
||||
|
||||
const recipeStepInputSchema = z.object({
|
||||
instructionSv: z.string().min(3).max(1000),
|
||||
timerSeconds: z.number().int().min(5).max(86_400).optional(),
|
||||
temperatureC: z.number().int().min(30).max(350).optional(),
|
||||
tip: z.string().max(300).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Användarrecept (spec §35): antingen strukturerat direkt, eller fritext som
|
||||
* AI strukturerar (via AAMOS) och användaren sedan granskar.
|
||||
*/
|
||||
export const createUserRecipeInputSchema = z.discriminatedUnion("mode", [
|
||||
z.object({
|
||||
mode: z.literal("structured"),
|
||||
titleSv: z.string().min(3).max(150),
|
||||
descriptionSv: z.string().max(2000).default(""),
|
||||
cuisine: z.enum(CUISINES).default("international"),
|
||||
mealTypes: z.array(z.enum(MEAL_TYPES)).min(1),
|
||||
tags: z.array(z.enum(RECIPE_TAGS)).default([]),
|
||||
methods: z.array(z.enum(COOKING_METHODS)).default([]),
|
||||
equipment: z.array(z.enum(EQUIPMENT)).default([]),
|
||||
difficulty: z.enum(RECIPE_DIFFICULTIES).default("easy"),
|
||||
prepTimeMinutes: z.number().int().min(0).max(600),
|
||||
cookTimeMinutes: z.number().int().min(0).max(1440),
|
||||
portions: z.number().int().min(1).max(24),
|
||||
spiceLevel: z.number().int().min(0).max(5).default(0),
|
||||
ingredients: z.array(recipeIngredientInputSchema).min(1).max(60),
|
||||
steps: z.array(recipeStepInputSchema).min(1).max(40),
|
||||
}),
|
||||
z.object({
|
||||
mode: z.literal("free_text"),
|
||||
text: z.string().min(20).max(8000),
|
||||
}),
|
||||
]);
|
||||
export type CreateUserRecipeInput = z.infer<typeof createUserRecipeInputSchema>;
|
||||
|
||||
export const rateRecipeInputSchema = z.object({
|
||||
stars: z.number().int().min(1).max(5),
|
||||
feedbackTags: z.array(z.enum(FEEDBACK_TAGS)).max(8).default([]),
|
||||
comment: z.string().max(1000).optional(),
|
||||
});
|
||||
export type RateRecipeInput = z.infer<typeof rateRecipeInputSchema>;
|
||||
|
||||
/** "Jag har lagat detta" – kärnflödet som drar lager och loggar måltid (spec §23). */
|
||||
export const cookRecipeInputSchema = z.object({
|
||||
portionsCooked: z.number().int().min(1).max(24),
|
||||
/** Vilka i hushållet som åt, med portionsandel per person. */
|
||||
eaters: z
|
||||
.array(
|
||||
z.object({
|
||||
userId: uuidSchema,
|
||||
portionFraction: z.number().min(0.1).max(3).default(1),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
/** Portioner som blev matlådor (spec §24). */
|
||||
mealBoxPortions: z.number().int().min(0).max(24).default(0),
|
||||
mealBoxStorageLocationId: uuidSchema.optional(),
|
||||
mealBoxFrozen: z.boolean().default(false),
|
||||
/** Dra ingredienser från lagret? Förifyllt förslag visas i appen. */
|
||||
deductInventory: z.boolean().default(true),
|
||||
/** Justeringar av vad som faktiskt användes. */
|
||||
inventoryOverrides: z
|
||||
.array(
|
||||
z.object({
|
||||
canonicalIngredientId: z.string().max(80),
|
||||
quantityUsed: z.number().min(0),
|
||||
unit: unitSchema,
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
date: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.optional(),
|
||||
mealType: z.enum(MEAL_TYPES).default("dinner"),
|
||||
});
|
||||
export type CookRecipeInput = z.infer<typeof cookRecipeInputSchema>;
|
||||
|
||||
export const substitutionQuerySchema = z.object({
|
||||
fromIngredientId: z.string().min(1).max(80),
|
||||
context: z.string().max(60).optional(),
|
||||
});
|
||||
export type SubstitutionQuery = z.infer<typeof substitutionQuerySchema>;
|
||||
Reference in New Issue
Block a user