637 lines
26 KiB
TypeScript
637 lines
26 KiB
TypeScript
|
|
import {
|
||
|
|
boolean,
|
||
|
|
date,
|
||
|
|
index,
|
||
|
|
integer,
|
||
|
|
jsonb,
|
||
|
|
numeric,
|
||
|
|
pgTable,
|
||
|
|
primaryKey,
|
||
|
|
text,
|
||
|
|
timestamp,
|
||
|
|
uniqueIndex,
|
||
|
|
} from "drizzle-orm/pg-core";
|
||
|
|
import { user } from "./auth";
|
||
|
|
import {
|
||
|
|
activityLevelEnum,
|
||
|
|
activitySourceEnum,
|
||
|
|
coachPriorityEnum,
|
||
|
|
dietStyleEnum,
|
||
|
|
foodSourceEnum,
|
||
|
|
healthGoalEnum,
|
||
|
|
mealTypeEnum,
|
||
|
|
portionUnitEnum,
|
||
|
|
reminderKindEnum,
|
||
|
|
wearableProviderEnum,
|
||
|
|
} from "./enums";
|
||
|
|
|
||
|
|
export const healthProfiles = pgTable("health_profiles", {
|
||
|
|
userId: text("userId").primaryKey().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
goal: healthGoalEnum("goal").notNull().default("healthier"),
|
||
|
|
age: integer("age"),
|
||
|
|
heightCm: numeric("heightCm", { precision: 6, scale: 2 }),
|
||
|
|
currentWeightKg: numeric("currentWeightKg", { precision: 6, scale: 2 }),
|
||
|
|
targetWeightKg: numeric("targetWeightKg", { precision: 6, scale: 2 }),
|
||
|
|
activityLevel: activityLevelEnum("activityLevel").notNull().default("moderate"),
|
||
|
|
dietStyle: dietStyleEnum("dietStyle").notNull().default("flexible"),
|
||
|
|
dietPreferences: jsonb("dietPreferences").$type<string[]>().notNull().default(["vegetarian", "vegan", "meat", "fish"]),
|
||
|
|
mealsPerDay: integer("mealsPerDay").notNull().default(3),
|
||
|
|
cooksPerDay: integer("cooksPerDay").notNull().default(1),
|
||
|
|
preferredMealPrepDays: integer("preferredMealPrepDays").notNull().default(0),
|
||
|
|
meatFishFrequency: text("meatFishFrequency").notNull().default("flexible"),
|
||
|
|
cookingTime: text("cookingTime").notNull().default("medium"),
|
||
|
|
budgetFocus: boolean("budgetFocus").notNull().default(false),
|
||
|
|
excludedFoods: text("excludedFoods"),
|
||
|
|
biggestChallenge: text("biggestChallenge"),
|
||
|
|
medicalNote: boolean("medicalNote").notNull().default(false),
|
||
|
|
completedAt: timestamp("completedAt"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const healthGoals = pgTable("health_goals", {
|
||
|
|
userId: text("userId").primaryKey().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
drinkMl: integer("drinkMl").notNull().default(2500),
|
||
|
|
calories: integer("calories").notNull().default(2000),
|
||
|
|
carbsG: integer("carbsG").notNull().default(200),
|
||
|
|
fatG: integer("fatG").notNull().default(67),
|
||
|
|
proteinG: integer("proteinG").notNull().default(150),
|
||
|
|
weightKg: numeric("weightKg", { precision: 6, scale: 2 }),
|
||
|
|
fastingHours: integer("fastingHours").notNull().default(12),
|
||
|
|
weeklyCalories: integer("weeklyCalories"),
|
||
|
|
sportAddsCalories: boolean("sportAddsCalories").notNull().default(true),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const foods = pgTable(
|
||
|
|
"foods",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
ownerUserId: text("ownerUserId").references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
brand: text("brand"),
|
||
|
|
barcode: text("barcode"),
|
||
|
|
category: text("category").notNull().default("Sonstiges"),
|
||
|
|
kcalPer100g: numeric("kcalPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
proteinPer100g: numeric("proteinPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
carbsPer100g: numeric("carbsPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
fatPer100g: numeric("fatPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
servingG: numeric("servingG", { precision: 8, scale: 2 }).notNull().default("100"),
|
||
|
|
preferredUnit: portionUnitEnum("preferredUnit").notNull().default("g"),
|
||
|
|
densityGPerMl: numeric("densityGPerMl", { precision: 8, scale: 4 }),
|
||
|
|
source: foodSourceEnum("source").notNull().default("manual"),
|
||
|
|
isVerified: boolean("isVerified").notNull().default(false),
|
||
|
|
isCommunity: boolean("isCommunity").notNull().default(false),
|
||
|
|
availableEverywhere: boolean("availableEverywhere").notNull().default(true),
|
||
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
ownerIdx: index("foods_owner_idx").on(table.ownerUserId),
|
||
|
|
barcodeIdx: index("foods_barcode_idx").on(table.barcode),
|
||
|
|
nameIdx: index("foods_name_idx").on(table.name),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const foodPortions = pgTable(
|
||
|
|
"food_portions",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
foodId: text("foodId").notNull().references(() => foods.id, { onDelete: "cascade" }),
|
||
|
|
label: text("label").notNull(),
|
||
|
|
unit: portionUnitEnum("unit").notNull(),
|
||
|
|
amount: numeric("amount", { precision: 8, scale: 2 }).notNull(),
|
||
|
|
gramsEquivalent: numeric("gramsEquivalent", { precision: 8, scale: 2 }).notNull(),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
foodIdx: index("food_portions_food_idx").on(table.foodId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const foodNutritionSources = pgTable(
|
||
|
|
"food_nutrition_sources",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
foodId: text("foodId").notNull().references(() => foods.id, { onDelete: "cascade" }),
|
||
|
|
provider: text("provider").notNull(),
|
||
|
|
providerFoodId: text("providerFoodId"),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
brand: text("brand"),
|
||
|
|
url: text("url"),
|
||
|
|
kcalPer100g: numeric("kcalPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
proteinPer100g: numeric("proteinPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
carbsPer100g: numeric("carbsPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
fatPer100g: numeric("fatPer100g", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
servingG: numeric("servingG", { precision: 8, scale: 2 }),
|
||
|
|
confidence: integer("confidence").notNull().default(70),
|
||
|
|
raw: jsonb("raw").$type<Record<string, unknown>>().notNull().default({}),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
foodIdx: index("food_nutrition_sources_food_idx").on(table.foodId),
|
||
|
|
providerIdx: index("food_nutrition_sources_provider_idx").on(table.provider, table.providerFoodId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const foodUserPreferences = pgTable(
|
||
|
|
"food_user_preferences",
|
||
|
|
{
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
foodId: text("foodId").notNull().references(() => foods.id, { onDelete: "cascade" }),
|
||
|
|
vote: integer("vote").notNull().default(0),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
pk: primaryKey({ columns: [table.userId, table.foodId] }),
|
||
|
|
userIdx: index("food_user_preferences_user_idx").on(table.userId),
|
||
|
|
foodIdx: index("food_user_preferences_food_idx").on(table.foodId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const stores = pgTable("stores", {
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
ownerUserId: text("ownerUserId").references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
address: text("address"),
|
||
|
|
placeId: text("placeId"),
|
||
|
|
latitude: numeric("latitude", { precision: 9, scale: 6 }),
|
||
|
|
longitude: numeric("longitude", { precision: 9, scale: 6 }),
|
||
|
|
favorite: boolean("favorite").notNull().default(false),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const storeChangeRequests = pgTable(
|
||
|
|
"store_change_requests",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
storeId: text("storeId").references(() => stores.id, { onDelete: "set null" }),
|
||
|
|
requestType: text("requestType").notNull().default("structure_update"),
|
||
|
|
status: text("status").notNull().default("pending"),
|
||
|
|
title: text("title").notNull(),
|
||
|
|
note: text("note"),
|
||
|
|
proposedStore: jsonb("proposedStore").$type<Record<string, unknown>>().notNull().default({}),
|
||
|
|
proposedDepartments: jsonb("proposedDepartments").$type<Array<Record<string, unknown>>>().notNull().default([]),
|
||
|
|
reviewedBy: text("reviewedBy").references(() => user.id, { onDelete: "set null" }),
|
||
|
|
reviewedAt: timestamp("reviewedAt"),
|
||
|
|
reviewNote: text("reviewNote"),
|
||
|
|
acceptedStoreId: text("acceptedStoreId").references(() => stores.id, { onDelete: "set null" }),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userIdx: index("store_change_requests_user_idx").on(table.userId),
|
||
|
|
statusIdx: index("store_change_requests_status_idx").on(table.status, table.createdAt),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const storeUserPreferences = pgTable(
|
||
|
|
"store_user_preferences",
|
||
|
|
{
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
storeId: text("storeId").notNull().references(() => stores.id, { onDelete: "cascade" }),
|
||
|
|
favorite: boolean("favorite").notNull().default(false),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
pk: primaryKey({ columns: [table.userId, table.storeId] }),
|
||
|
|
userIdx: index("store_user_preferences_user_idx").on(table.userId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const storeDepartments = pgTable(
|
||
|
|
"store_departments",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
storeId: text("storeId").notNull().references(() => stores.id, { onDelete: "cascade" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
sortOrder: integer("sortOrder").notNull().default(0),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
storeIdx: index("store_departments_store_idx").on(table.storeId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const foodStores = pgTable(
|
||
|
|
"food_stores",
|
||
|
|
{
|
||
|
|
foodId: text("foodId").notNull().references(() => foods.id, { onDelete: "cascade" }),
|
||
|
|
storeId: text("storeId").notNull().references(() => stores.id, { onDelete: "cascade" }),
|
||
|
|
departmentId: text("departmentId").references(() => storeDepartments.id, { onDelete: "set null" }),
|
||
|
|
lastPrice: numeric("lastPrice", { precision: 8, scale: 2 }),
|
||
|
|
availabilityTags: jsonb("availabilityTags").$type<string[]>().notNull().default([]),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
pk: primaryKey({ columns: [table.foodId, table.storeId] }),
|
||
|
|
storeIdx: index("food_stores_store_idx").on(table.storeId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const recipes = pgTable(
|
||
|
|
"recipes",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
ownerUserId: text("ownerUserId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
portions: integer("portions").notNull().default(1),
|
||
|
|
minutes: integer("minutes").notNull().default(0),
|
||
|
|
notes: text("notes"),
|
||
|
|
mealPrepDays: integer("mealPrepDays").notNull().default(0),
|
||
|
|
isPublic: boolean("isPublic").notNull().default(false),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
ownerIdx: index("recipes_owner_idx").on(table.ownerUserId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const recipeIngredients = pgTable(
|
||
|
|
"recipe_ingredients",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
recipeId: text("recipeId").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||
|
|
foodId: text("foodId").notNull().references(() => foods.id, { onDelete: "restrict" }),
|
||
|
|
amountG: numeric("amountG", { precision: 8, scale: 2 }).notNull(),
|
||
|
|
displayAmount: numeric("displayAmount", { precision: 8, scale: 2 }),
|
||
|
|
displayUnit: portionUnitEnum("displayUnit"),
|
||
|
|
portionId: text("portionId").references(() => foodPortions.id, { onDelete: "set null" }),
|
||
|
|
sortOrder: integer("sortOrder").notNull().default(0),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
recipeIdx: index("recipe_ingredients_recipe_idx").on(table.recipeId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const recipeSubmissions = pgTable(
|
||
|
|
"recipe_submissions",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
portions: integer("portions").notNull().default(1),
|
||
|
|
minutes: integer("minutes").notNull().default(0),
|
||
|
|
notes: text("notes"),
|
||
|
|
mealPrepDays: integer("mealPrepDays").notNull().default(0),
|
||
|
|
status: text("status").notNull().default("pending"),
|
||
|
|
reviewNote: text("reviewNote"),
|
||
|
|
reviewedBy: text("reviewedBy").references(() => user.id, { onDelete: "set null" }),
|
||
|
|
reviewedAt: timestamp("reviewedAt"),
|
||
|
|
acceptedRecipeId: text("acceptedRecipeId").references(() => recipes.id, { onDelete: "set null" }),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userIdx: index("recipe_submissions_user_idx").on(table.userId),
|
||
|
|
statusIdx: index("recipe_submissions_status_idx").on(table.status, table.createdAt),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const recipeSubmissionIngredients = pgTable(
|
||
|
|
"recipe_submission_ingredients",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
submissionId: text("submissionId").notNull().references(() => recipeSubmissions.id, { onDelete: "cascade" }),
|
||
|
|
foodId: text("foodId").notNull().references(() => foods.id, { onDelete: "restrict" }),
|
||
|
|
amountG: numeric("amountG", { precision: 8, scale: 2 }).notNull(),
|
||
|
|
displayAmount: numeric("displayAmount", { precision: 8, scale: 2 }),
|
||
|
|
displayUnit: portionUnitEnum("displayUnit"),
|
||
|
|
portionId: text("portionId").references(() => foodPortions.id, { onDelete: "set null" }),
|
||
|
|
sortOrder: integer("sortOrder").notNull().default(0),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
submissionIdx: index("recipe_submission_ingredients_submission_idx").on(table.submissionId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const recipeRatings = pgTable(
|
||
|
|
"recipe_ratings",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
recipeId: text("recipeId").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
rating: integer("rating").notNull(),
|
||
|
|
comment: text("comment"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
uniqueUserRecipe: uniqueIndex("recipe_ratings_user_recipe_unique").on(table.recipeId, table.userId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const mealPlans = pgTable(
|
||
|
|
"meal_plans",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
mealType: mealTypeEnum("mealType").notNull(),
|
||
|
|
recipeId: text("recipeId").references(() => recipes.id, { onDelete: "set null" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
portions: numeric("portions", { precision: 8, scale: 2 }).notNull().default("1"),
|
||
|
|
plannedCalories: integer("plannedCalories").notNull().default(0),
|
||
|
|
note: text("note"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userDateIdx: index("meal_plans_user_date_idx").on(table.userId, table.date),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const mealLogs = pgTable(
|
||
|
|
"meal_logs",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
mealType: mealTypeEnum("mealType").notNull(),
|
||
|
|
foodId: text("foodId").references(() => foods.id, { onDelete: "set null" }),
|
||
|
|
recipeId: text("recipeId").references(() => recipes.id, { onDelete: "set null" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
amountLabel: text("amountLabel"),
|
||
|
|
amountG: numeric("amountG", { precision: 8, scale: 2 }),
|
||
|
|
calories: integer("calories").notNull().default(0),
|
||
|
|
proteinG: numeric("proteinG", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
carbsG: numeric("carbsG", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
fatG: numeric("fatG", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
source: text("source").notNull().default("manual"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userDateIdx: index("meal_logs_user_date_idx").on(table.userId, table.date),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const drinkLogs = pgTable(
|
||
|
|
"drink_logs",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
amountMl: integer("amountMl").notNull(),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userDateIdx: index("drink_logs_user_date_idx").on(table.userId, table.date),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const weightLogs = pgTable(
|
||
|
|
"weight_logs",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
weightKg: numeric("weightKg", { precision: 6, scale: 2 }).notNull(),
|
||
|
|
note: text("note"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
uniqueUserDate: uniqueIndex("weight_logs_user_date_unique").on(table.userId, table.date),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const weeklyCaloriePlans = pgTable(
|
||
|
|
"weekly_calorie_plans",
|
||
|
|
{
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
plannedCalories: integer("plannedCalories").notNull(),
|
||
|
|
note: text("note"),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
pk: primaryKey({ columns: [table.userId, table.date] }),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const activityTemplates = pgTable(
|
||
|
|
"activity_templates",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
ownerUserId: text("ownerUserId").references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
category: text("category").notNull().default("habit"),
|
||
|
|
defaultMinutes: integer("defaultMinutes").notNull().default(10),
|
||
|
|
defaultPoints: integer("defaultPoints").notNull().default(10),
|
||
|
|
defaultCalories: integer("defaultCalories").notNull().default(0),
|
||
|
|
icon: text("icon").notNull().default("custom"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
ownerIdx: index("activity_templates_owner_idx").on(table.ownerUserId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const activityLogs = pgTable(
|
||
|
|
"activity_logs",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
templateId: text("templateId").references(() => activityTemplates.id, { onDelete: "set null" }),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
minutes: integer("minutes").notNull().default(0),
|
||
|
|
points: integer("points").notNull().default(0),
|
||
|
|
calories: integer("calories").notNull().default(0),
|
||
|
|
source: activitySourceEnum("source").notNull().default("manual"),
|
||
|
|
externalId: text("externalId"),
|
||
|
|
note: text("note"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userDateIdx: index("activity_logs_user_date_idx").on(table.userId, table.date),
|
||
|
|
externalUnique: uniqueIndex("activity_logs_source_external_unique").on(table.userId, table.source, table.externalId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const shoppingLists = pgTable(
|
||
|
|
"shopping_lists",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
storeId: text("storeId").references(() => stores.id, { onDelete: "set null" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
startsOn: date("startsOn"),
|
||
|
|
endsOn: date("endsOn"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userIdx: index("shopping_lists_user_idx").on(table.userId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const shoppingListItems = pgTable(
|
||
|
|
"shopping_list_items",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
shoppingListId: text("shoppingListId").notNull().references(() => shoppingLists.id, { onDelete: "cascade" }),
|
||
|
|
foodId: text("foodId").references(() => foods.id, { onDelete: "set null" }),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
category: text("category").notNull().default("Sonstiges"),
|
||
|
|
amountG: numeric("amountG", { precision: 8, scale: 2 }).notNull().default("0"),
|
||
|
|
purchaseAmount: text("purchaseAmount"),
|
||
|
|
checked: boolean("checked").notNull().default(false),
|
||
|
|
sortOrder: integer("sortOrder").notNull().default(0),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
listIdx: index("shopping_list_items_list_idx").on(table.shoppingListId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const wearableConnections = pgTable(
|
||
|
|
"wearable_connections",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
provider: wearableProviderEnum("provider").notNull(),
|
||
|
|
providerUserId: text("providerUserId"),
|
||
|
|
accessTokenEncrypted: text("accessTokenEncrypted"),
|
||
|
|
refreshTokenEncrypted: text("refreshTokenEncrypted"),
|
||
|
|
scopes: jsonb("scopes").$type<string[]>().notNull().default([]),
|
||
|
|
expiresAt: timestamp("expiresAt"),
|
||
|
|
lastSyncAt: timestamp("lastSyncAt"),
|
||
|
|
syncEnabled: boolean("syncEnabled").notNull().default(true),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
uniqueProvider: uniqueIndex("wearable_connections_user_provider_unique").on(table.userId, table.provider),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const wearableImports = pgTable(
|
||
|
|
"wearable_imports",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
connectionId: text("connectionId").notNull().references(() => wearableConnections.id, { onDelete: "cascade" }),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
provider: wearableProviderEnum("provider").notNull(),
|
||
|
|
externalId: text("externalId").notNull(),
|
||
|
|
date: date("date").notNull(),
|
||
|
|
payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
|
||
|
|
importedAt: timestamp("importedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
uniqueExternal: uniqueIndex("wearable_imports_provider_external_unique").on(table.provider, table.externalId),
|
||
|
|
userDateIdx: index("wearable_imports_user_date_idx").on(table.userId, table.date),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const coachPrinciples = pgTable("coach_principles", {
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
title: text("title").notNull(),
|
||
|
|
body: text("body").notNull(),
|
||
|
|
category: text("category").notNull().default("general"),
|
||
|
|
isActive: boolean("isActive").notNull().default(true),
|
||
|
|
sortOrder: integer("sortOrder").notNull().default(0),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const coachRecommendations = pgTable(
|
||
|
|
"coach_recommendations",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
title: text("title").notNull(),
|
||
|
|
body: text("body").notNull(),
|
||
|
|
priority: coachPriorityEnum("priority").notNull().default("medium"),
|
||
|
|
category: text("category").notNull().default("daily"),
|
||
|
|
source: text("source").notNull().default("rule"),
|
||
|
|
validFrom: date("validFrom"),
|
||
|
|
validTo: date("validTo"),
|
||
|
|
dismissedAt: timestamp("dismissedAt"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
userIdx: index("coach_recommendations_user_idx").on(table.userId),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const reminderRules = pgTable(
|
||
|
|
"reminder_rules",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
kind: reminderKindEnum("kind").notNull(),
|
||
|
|
enabled: boolean("enabled").notNull().default(true),
|
||
|
|
intervalMinutes: integer("intervalMinutes").notNull().default(90),
|
||
|
|
startTime: text("startTime").notNull().default("08:00"),
|
||
|
|
endTime: text("endTime").notNull().default("20:00"),
|
||
|
|
message: text("message").notNull(),
|
||
|
|
quietDays: jsonb("quietDays").$type<string[]>().notNull().default([]),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
uniqueKind: uniqueIndex("reminder_rules_user_kind_unique").on(table.userId, table.kind),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const clubActivities = pgTable(
|
||
|
|
"club_activities",
|
||
|
|
{
|
||
|
|
id: text("id").primaryKey(),
|
||
|
|
hostUserId: text("hostUserId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
title: text("title").notNull(),
|
||
|
|
category: text("category").notNull().default("other"),
|
||
|
|
scope: text("scope").notNull().default("nationwide"),
|
||
|
|
state: text("state"),
|
||
|
|
city: text("city"),
|
||
|
|
latitude: numeric("latitude", { precision: 9, scale: 6 }),
|
||
|
|
longitude: numeric("longitude", { precision: 9, scale: 6 }),
|
||
|
|
locationLabel: text("locationLabel"),
|
||
|
|
startsAt: timestamp("startsAt").notNull(),
|
||
|
|
durationMinutes: integer("durationMinutes").notNull().default(30),
|
||
|
|
points: integer("points").notNull().default(20),
|
||
|
|
note: text("note"),
|
||
|
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
startsAtIdx: index("club_activities_starts_at_idx").on(table.startsAt),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export const clubProfiles = pgTable("club_profiles", {
|
||
|
|
userId: text("userId").primaryKey().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
state: text("state").notNull().default("Bayern"),
|
||
|
|
city: text("city"),
|
||
|
|
latitude: numeric("latitude", { precision: 9, scale: 6 }),
|
||
|
|
longitude: numeric("longitude", { precision: 9, scale: 6 }),
|
||
|
|
privacyMode: text("privacyMode").notNull().default("state"),
|
||
|
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const clubActivityParticipants = pgTable(
|
||
|
|
"club_activity_participants",
|
||
|
|
{
|
||
|
|
activityId: text("activityId").notNull().references(() => clubActivities.id, { onDelete: "cascade" }),
|
||
|
|
userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||
|
|
displayName: text("displayName").notNull(),
|
||
|
|
joinedAt: timestamp("joinedAt").notNull().defaultNow(),
|
||
|
|
completedAt: timestamp("completedAt"),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
pk: primaryKey({ columns: [table.activityId, table.userId] }),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
export type HealthProfile = typeof healthProfiles.$inferSelect;
|
||
|
|
export type NewHealthProfile = typeof healthProfiles.$inferInsert;
|
||
|
|
export type HealthGoal = typeof healthGoals.$inferSelect;
|
||
|
|
export type NewHealthGoal = typeof healthGoals.$inferInsert;
|
||
|
|
export type Food = typeof foods.$inferSelect;
|
||
|
|
export type NewFood = typeof foods.$inferInsert;
|
||
|
|
export type Recipe = typeof recipes.$inferSelect;
|
||
|
|
export type NewRecipe = typeof recipes.$inferInsert;
|