Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+132
View File
@@ -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>;