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
+231
View File
@@ -0,0 +1,231 @@
import {
boolean,
doublePrecision,
index,
integer,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import type { NutritionValues, RecipeDNA, Season } from "@app/shared-types";
import {
allergenEnum,
createdAt,
cuisineEnum,
mealTypeEnum,
recipeDifficultyEnum,
recipeSimilarityClassEnum,
recipeSourceTypeEnum,
recipeStatusEnum,
recipeVariantTypeEnum,
recipeVerificationStatusEnum,
unitEnum,
updatedAt,
} from "./_shared.js";
import { canonicalIngredients } from "./ingredients.js";
import { users } from "./users.js";
/** Juridiskt source registry (spec §15). */
export const recipeSourceRegistry = pgTable("recipe_source_registry", {
id: uuid("id").primaryKey().defaultRandom(),
sourceName: text("source_name").notNull(),
license: text("license").notNull(),
rightToStore: boolean("right_to_store").notNull(),
rightToModify: boolean("right_to_modify").notNull(),
rightToDisplay: boolean("right_to_display").notNull(),
attributionRequired: boolean("attribution_required").notNull().default(false),
attributionText: text("attribution_text"),
commercialUse: boolean("commercial_use").notNull(),
validFrom: timestamp("valid_from", { withTimezone: true }).notNull().defaultNow(),
validTo: timestamp("valid_to", { withTimezone: true }),
notes: text("notes"),
createdAt: createdAt(),
});
export const recipes = pgTable(
"recipes",
{
id: uuid("id").primaryKey().defaultRandom(),
slug: text("slug").notNull(),
titleSv: text("title_sv").notNull(),
descriptionSv: text("description_sv").notNull().default(""),
country: text("country"),
region: text("region"),
cuisine: cuisineEnum("cuisine").notNull(),
mealTypes: mealTypeEnum("meal_types").array().notNull().default([]),
tags: text("tags").array().notNull().default([]),
methods: text("methods").array().notNull().default([]),
equipment: text("equipment").array().notNull().default([]),
difficulty: recipeDifficultyEnum("difficulty").notNull().default("easy"),
prepTimeMinutes: integer("prep_time_minutes").notNull().default(0),
cookTimeMinutes: integer("cook_time_minutes").notNull().default(0),
totalTimeMinutes: integer("total_time_minutes").notNull().default(0),
portions: integer("portions").notNull().default(4),
/** Beräknas ALLTID deterministiskt av nutrition-engine (spec §61.1). */
nutritionPerPortion: jsonb("nutrition_per_portion").$type<NutritionValues>().notNull(),
/** Härledda ur ingrediensernas allergener deterministiskt (spec §61.2). */
allergens: allergenEnum("allergens").array().notNull().default([]),
spiceLevel: integer("spice_level").notNull().default(0),
estimatedCostMinorPerPortion: integer("estimated_cost_minor_per_portion"),
storageGuidanceSv: text("storage_guidance_sv"),
mealPrepFriendly: boolean("meal_prep_friendly").notNull().default(false),
freezerFriendly: boolean("freezer_friendly").notNull().default(false),
peakSeasons: text("peak_seasons").array().$type<Season[]>().notNull().default([]),
holidayTags: text("holiday_tags").array().notNull().default([]),
dna: jsonb("dna").$type<RecipeDNA>().notNull(),
variantType: recipeVariantTypeEnum("variant_type").notNull().default("standard"),
variantOfRecipeId: uuid("variant_of_recipe_id"),
forkedFromRecipeId: uuid("forked_from_recipe_id"),
status: recipeStatusEnum("status").notNull().default("draft"),
verificationStatus: recipeVerificationStatusEnum("verification_status")
.notNull()
.default("unverified"),
sourceType: recipeSourceTypeEnum("source_type").notNull(),
sourceRegistryId: uuid("source_registry_id").references(() => recipeSourceRegistry.id),
creatorUserId: uuid("creator_user_id").references(() => users.id, { onDelete: "set null" }),
creatorDisplayName: text("creator_display_name"),
imageUrls: text("image_urls").array().notNull().default([]),
version: integer("version").notNull().default(1),
ratingAverage: doublePrecision("rating_average"),
ratingCount: integer("rating_count").notNull().default(0),
cookCount: integer("cook_count").notNull().default(0),
favoriteCount: integer("favorite_count").notNull().default(0),
moderationNote: text("moderation_note"),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [
uniqueIndex("recipes_slug_unique").on(t.slug),
index("recipes_status_idx").on(t.status),
index("recipes_cuisine_idx").on(t.cuisine),
index("recipes_variant_of_idx").on(t.variantOfRecipeId),
index("recipes_creator_idx").on(t.creatorUserId),
index("recipes_total_time_idx").on(t.totalTimeMinutes),
],
);
export const recipeIngredients = pgTable(
"recipe_ingredients",
{
id: uuid("id").primaryKey().defaultRandom(),
recipeId: uuid("recipe_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
canonicalIngredientId: text("canonical_ingredient_id")
.notNull()
.references(() => canonicalIngredients.id),
displayNameSv: text("display_name_sv").notNull(),
quantity: doublePrecision("quantity").notNull(),
unit: unitEnum("unit").notNull(),
note: text("note"),
optional: boolean("optional").notNull().default(false),
groupName: text("group_name"),
sortOrder: integer("sort_order").notNull().default(0),
},
(t) => [
index("recipe_ingredients_recipe_idx").on(t.recipeId),
index("recipe_ingredients_canonical_idx").on(t.canonicalIngredientId),
],
);
export const recipeSteps = pgTable(
"recipe_steps",
{
id: uuid("id").primaryKey().defaultRandom(),
recipeId: uuid("recipe_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
stepNumber: integer("step_number").notNull(),
instructionSv: text("instruction_sv").notNull(),
timerSeconds: integer("timer_seconds"),
temperatureC: integer("temperature_c"),
tip: text("tip"),
},
(t) => [
index("recipe_steps_recipe_idx").on(t.recipeId),
uniqueIndex("recipe_steps_recipe_step_unique").on(t.recipeId, t.stepNumber),
],
);
export const recipeRatings = pgTable(
"recipe_ratings",
{
id: uuid("id").primaryKey().defaultRandom(),
recipeId: uuid("recipe_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
stars: integer("stars").notNull(),
feedbackTags: text("feedback_tags").array().notNull().default([]),
comment: text("comment"),
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [
uniqueIndex("recipe_ratings_user_recipe_unique").on(t.recipeId, t.userId),
index("recipe_ratings_recipe_idx").on(t.recipeId),
],
);
export const recipeFavorites = pgTable(
"recipe_favorites",
{
recipeId: uuid("recipe_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: createdAt(),
},
(t) => [
primaryKey({ columns: [t.recipeId, t.userId] }),
index("recipe_favorites_user_idx").on(t.userId),
],
);
/** Logg över lagningar grund för betyg, ranking, Food Memory (spec §3739). */
export const recipeCooks = pgTable(
"recipe_cooks",
{
id: uuid("id").primaryKey().defaultRandom(),
recipeId: uuid("recipe_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
householdId: uuid("household_id"),
portionsCooked: integer("portions_cooked").notNull(),
cookedAt: timestamp("cooked_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index("recipe_cooks_recipe_idx").on(t.recipeId),
index("recipe_cooks_user_idx").on(t.userId),
],
);
/** Dubblett-/variantklassning mellan recept (spec §36). */
export const recipeSimilarities = pgTable(
"recipe_similarities",
{
id: uuid("id").primaryKey().defaultRandom(),
recipeAId: uuid("recipe_a_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
recipeBId: uuid("recipe_b_id")
.notNull()
.references(() => recipes.id, { onDelete: "cascade" }),
similarityScore: doublePrecision("similarity_score").notNull(),
classification: recipeSimilarityClassEnum("classification").notNull(),
details: jsonb("details"),
createdAt: createdAt(),
},
(t) => [uniqueIndex("recipe_similarities_pair_unique").on(t.recipeAId, t.recipeBId)],
);