From 634e67c6024ff9433dae25bd037e02f198f1f8e7 Mon Sep 17 00:00:00 2001 From: Sebastian Mayer Date: Sat, 30 May 2026 11:57:24 +0200 Subject: [PATCH] Feature: Community-Profil-Fundament (Handle, Bio, Sichtbarkeit) - Backend Battle.net-Style Handle Username#XXXX als Basis fuer kommende Community-Features (Stufe 2 der Roadmap). Diese Stufe: Schema, Migration, Generator und API. Die UI im Profil-Tab folgt in einem eigenen Commit. Schema (packages/db/src/schema/auth.ts): - Neue Spalten an der user-Tabelle: username, discriminator, bio (alle nullable), profileVisible (notNull, default false = Privacy-by-Default). - uniqueIndex auf (username, discriminator); NULLs distinct, greift also nur bei vollstaendigen Handles. Index user_username_idx auf username fuer die Handle-Erstvergabe-Query. - Migration 0019_community_profile, per drizzle-kit generate erzeugt, driftfrei (zweiter generate: keine Aenderung), drizzle-kit check ok. Noch nicht gegen die DB angewendet. Generator (packages/api/src/lib/handle.ts): - Discriminator aus verwechslungsarmem Alphabet, Kollisions-Retry. - formatHandle, validateUsername. 17 node:test-Unit-Tests. API (packages/api/src/trpc/routers/users.ts): - myProfile um die Felder + abgeleitetes handle erweitert (Feld-Whitelist). - setMyHandle: Discriminator stabil bei Erst-Vergabe, Kollisionspruefung inkl. DB-unique-violation-Fallback (PG 23505). - updateMyCommunityProfile: bio + Sichtbarkeit, strikt userId-gescoped. Verifiziert: Typecheck gruen (db/api/admin), 62 API-Tests gruen, drizzle-kit check ok. Datenmodell-Architekt abgenommen (Index-Auflage erfuellt). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/api/src/lib/handle.ts | 98 + packages/api/src/trpc/handle.test.ts | 102 + packages/api/src/trpc/routers/users.ts | 132 + .../db/drizzle/0019_community_profile.sql | 6 + packages/db/drizzle/meta/0019_snapshot.json | 4686 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/auth.ts | 71 +- 7 files changed, 5081 insertions(+), 21 deletions(-) create mode 100644 packages/api/src/lib/handle.ts create mode 100644 packages/api/src/trpc/handle.test.ts create mode 100644 packages/db/drizzle/0019_community_profile.sql create mode 100644 packages/db/drizzle/meta/0019_snapshot.json diff --git a/packages/api/src/lib/handle.ts b/packages/api/src/lib/handle.ts new file mode 100644 index 0000000..b3f6c57 --- /dev/null +++ b/packages/api/src/lib/handle.ts @@ -0,0 +1,98 @@ +// Community-Handle im Battle.net-Stil: "Username#XXXX". +// +// Der Username ist frei wählbar und darf mehrfach vorkommen. Eindeutig wird +// ein Nutzer erst durch die Kombination Username + Discriminator. Der +// Discriminator wird einmalig bei der Erst-Vergabe erzeugt und bleibt danach +// stabil, auch wenn der Username später geändert wird. + +// Verwechslungsarmes Alphabet: kein 0/O, 1/I/L, damit Handles vorlesbar bleiben. +export const DISCRIMINATOR_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; +export const DISCRIMINATOR_LENGTH = 4; + +export const USERNAME_MIN_LENGTH = 3; +export const USERNAME_MAX_LENGTH = 24; +export const BIO_MAX_LENGTH = 280; + +// Erlaubt Buchstaben, Ziffern, Unterstrich und Bindestrich. Keine Leerzeichen, +// kein '#' (reserviert als Trenner), keine Umlaute (Handle bleibt URL-tauglich). +const USERNAME_PATTERN = /^[A-Za-z0-9_-]+$/; + +export type RandomFn = () => number; + +/** + * Erzeugt einen zufälligen Discriminator (z. B. "A12B"). + * + * `random` ist injizierbar, damit der Generator deterministisch getestet werden + * kann; im Betrieb wird Math.random verwendet. + */ +export function generateDiscriminator(random: RandomFn = Math.random): string { + let out = ""; + for (let i = 0; i < DISCRIMINATOR_LENGTH; i++) { + const idx = Math.floor(random() * DISCRIMINATOR_ALPHABET.length); + out += DISCRIMINATOR_ALPHABET[idx]; + } + return out; +} + +/** + * Erzeugt einen Discriminator, der noch nicht in `taken` vorkommt. + * + * `taken` enthält die bereits vergebenen Discriminatoren für genau diesen + * Username (Kollision ist nur innerhalb desselben Usernames relevant). Nach + * `maxAttempts` erfolglosen Versuchen wird ein Fehler geworfen, damit der + * Aufrufer das nicht still ignoriert. + */ +export function generateUniqueDiscriminator( + taken: Iterable, + random: RandomFn = Math.random, + maxAttempts = 50, +): string { + const used = new Set(); + for (const t of taken) used.add(t.toUpperCase()); + + // Wenn der gesamte Raum voll ist, gar nicht erst probieren. + const space = DISCRIMINATOR_ALPHABET.length ** DISCRIMINATOR_LENGTH; + if (used.size >= space) { + throw new Error("Keine freien Discriminatoren mehr für diesen Namen"); + } + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const candidate = generateDiscriminator(random); + if (!used.has(candidate)) return candidate; + } + throw new Error("Konnte keinen freien Discriminator erzeugen"); +} + +/** Liefert den anzeigefertigen Handle oder null, wenn (noch) keiner vergeben ist. */ +export function formatHandle( + username: string | null | undefined, + discriminator: string | null | undefined, +): string | null { + if (!username || !discriminator) return null; + return `${username}#${discriminator}`; +} + +export type UsernameValidation = + | { ok: true; value: string } + | { ok: false; error: string }; + +/** + * Prüft und normalisiert einen vom Nutzer gewählten Username. + * Trimmt Whitespace, lässt aber Groß-/Kleinschreibung wie eingegeben. + */ +export function validateUsername(raw: string): UsernameValidation { + const value = raw.trim(); + if (value.length < USERNAME_MIN_LENGTH) { + return { ok: false, error: `Mindestens ${USERNAME_MIN_LENGTH} Zeichen` }; + } + if (value.length > USERNAME_MAX_LENGTH) { + return { ok: false, error: `Höchstens ${USERNAME_MAX_LENGTH} Zeichen` }; + } + if (!USERNAME_PATTERN.test(value)) { + return { + ok: false, + error: "Nur Buchstaben, Ziffern, Unterstrich und Bindestrich erlaubt", + }; + } + return { ok: true, value }; +} diff --git a/packages/api/src/trpc/handle.test.ts b/packages/api/src/trpc/handle.test.ts new file mode 100644 index 0000000..5c41f95 --- /dev/null +++ b/packages/api/src/trpc/handle.test.ts @@ -0,0 +1,102 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + BIO_MAX_LENGTH, + DISCRIMINATOR_ALPHABET, + DISCRIMINATOR_LENGTH, + formatHandle, + generateDiscriminator, + generateUniqueDiscriminator, + validateUsername, +} from "../lib/handle"; + +/** Liefert eine deterministische Random-Funktion, die der Reihe nach `values` ausgibt. */ +function seededRandom(values: number[]): () => number { + let i = 0; + return () => values[i++ % values.length]; +} + +test("generateDiscriminator hat die konfigurierte Länge", () => { + assert.equal(generateDiscriminator().length, DISCRIMINATOR_LENGTH); +}); + +test("generateDiscriminator nutzt nur Zeichen aus dem Alphabet", () => { + for (let i = 0; i < 200; i++) { + for (const ch of generateDiscriminator()) { + assert.ok(DISCRIMINATOR_ALPHABET.includes(ch), `unerwartetes Zeichen: ${ch}`); + } + } +}); + +test("Alphabet enthält keine verwechselbaren Zeichen (0 O 1 I L)", () => { + for (const forbidden of ["0", "O", "1", "I", "L"]) { + assert.ok(!DISCRIMINATOR_ALPHABET.includes(forbidden), `verboten: ${forbidden}`); + } +}); + +test("generateDiscriminator ist deterministisch bei fester Random-Quelle", () => { + // random()=0 -> immer erstes Zeichen des Alphabets + assert.equal( + generateDiscriminator(() => 0), + DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH), + ); +}); + +test("generateUniqueDiscriminator umgeht bereits vergebene Discriminatoren", () => { + const first = DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH); + const second = + DISCRIMINATOR_ALPHABET[1] + DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH - 1); + // Erster Versuch kollidiert (alles idx 0), zweiter Versuch erstes Zeichen = idx 1 + const random = seededRandom([0, 0, 0, 0, 1 / DISCRIMINATOR_ALPHABET.length, 0, 0, 0]); + assert.equal(generateUniqueDiscriminator([first], random), second); +}); + +test("generateUniqueDiscriminator ignoriert Groß-/Kleinschreibung", () => { + const taken = DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH).toLowerCase(); + const random = seededRandom([0, 0, 0, 0, 1 / DISCRIMINATOR_ALPHABET.length, 0, 0, 0]); + const result = generateUniqueDiscriminator([taken], random); + assert.notEqual(result, DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH)); +}); + +test("generateUniqueDiscriminator wirft, wenn nach maxAttempts kein freier gefunden wird", () => { + const onlyZero = () => 0; // erzeugt immer denselben Discriminator + const taken = [DISCRIMINATOR_ALPHABET[0].repeat(DISCRIMINATOR_LENGTH)]; + assert.throws(() => generateUniqueDiscriminator(taken, onlyZero, 10)); +}); + +test("formatHandle baut Username#Discriminator", () => { + assert.equal(formatHandle("Held", "A12B"), "Held#A12B"); +}); + +test("formatHandle liefert null ohne Username", () => { + assert.equal(formatHandle(null, "A12B"), null); +}); + +test("formatHandle liefert null ohne Discriminator", () => { + assert.equal(formatHandle("Held", null), null); +}); + +test("validateUsername akzeptiert gültige Namen und trimmt", () => { + assert.deepEqual(validateUsername(" Held_2 "), { ok: true, value: "Held_2" }); +}); + +test("validateUsername lehnt zu kurze Namen ab", () => { + assert.equal(validateUsername("ab").ok, false); +}); + +test("validateUsername lehnt zu lange Namen ab", () => { + assert.equal(validateUsername("a".repeat(25)).ok, false); +}); + +test("validateUsername lehnt '#' und Leerzeichen ab", () => { + assert.equal(validateUsername("Held#1").ok, false); + assert.equal(validateUsername("Held Held").ok, false); +}); + +test("validateUsername lehnt Umlaute ab (Handle bleibt URL-tauglich)", () => { + assert.equal(validateUsername("Hähnchen").ok, false); +}); + +test("Bio-Limit ist 280", () => { + assert.equal(BIO_MAX_LENGTH, 280); +}); diff --git a/packages/api/src/trpc/routers/users.ts b/packages/api/src/trpc/routers/users.ts index c133916..5718002 100644 --- a/packages/api/src/trpc/routers/users.ts +++ b/packages/api/src/trpc/routers/users.ts @@ -3,6 +3,14 @@ import { TRPCError } from "@trpc/server"; import { asc, eq, sql } from "drizzle-orm"; import { z } from "zod"; import { auth } from "../../lib/auth"; +import { + BIO_MAX_LENGTH, + USERNAME_MAX_LENGTH, + USERNAME_MIN_LENGTH, + formatHandle, + generateUniqueDiscriminator, + validateUsername, +} from "../../lib/handle"; import { auditSecurityEvent } from "../../lib/security-audit"; import { platformProcedure, protectedProcedure, router } from "../init"; @@ -19,6 +27,17 @@ const imageFieldSchema = z const platformRoleSchema = z.enum(["user", "admin"]); +// Postgres-Fehlercode 23505 = unique_violation. Wird genutzt, um eine +// Race-Condition bei der Handle-Vergabe sauber in einen CONFLICT zu übersetzen. +function isUniqueViolation(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + (error as { code?: unknown }).code === "23505" + ); +} + export const usersRouter = router({ myProfile: protectedProcedure.query(async ({ ctx }) => { const user = await ctx.db.query.user.findFirst({ @@ -30,6 +49,10 @@ export const usersRouter = router({ image: true, role: true, theme: true, + username: true, + discriminator: true, + bio: true, + profileVisible: true, twoFactorEnabled: true, twoFactorRecoveryRequired: true, createdAt: true, @@ -50,6 +73,7 @@ export const usersRouter = router({ street: null, postalCode: null, city: null, + handle: formatHandle(user.username, user.discriminator), }; }), @@ -82,6 +106,114 @@ export const usersRouter = router({ return { ok: true }; }), + // Vergibt oder ändert den Community-Username. Der Discriminator wird einmalig + // bei der Erst-Vergabe erzeugt und bleibt danach stabil, auch wenn der + // Username später geändert wird (stabile Identität). + setMyHandle: protectedProcedure + .input( + z.object({ + username: z.string().trim().min(USERNAME_MIN_LENGTH).max(USERNAME_MAX_LENGTH), + }), + ) + .mutation(async ({ ctx, input }) => { + const validation = validateUsername(input.username); + if (!validation.ok) { + throw new TRPCError({ code: "BAD_REQUEST", message: validation.error }); + } + const username = validation.value; + + const me = await ctx.db.query.user.findFirst({ + where: (row, { eq: rowEq }) => rowEq(row.id, ctx.user.id), + columns: { discriminator: true }, + }); + if (!me) { + throw new TRPCError({ code: "NOT_FOUND", message: "Benutzer nicht gefunden" }); + } + + // Bestehenden Discriminator beibehalten (stabil). Nur bei Erst-Vergabe neu + // erzeugen, dann gegen die für diesen Username bereits vergebenen prüfen. + let discriminator: string; + if (!me.discriminator) { + const others = await ctx.db.query.user.findMany({ + where: (row, { eq: rowEq }) => rowEq(row.username, username), + columns: { discriminator: true }, + }); + const taken = others + .map((row) => row.discriminator) + .filter((d): d is string => Boolean(d)); + try { + discriminator = generateUniqueDiscriminator(taken); + } catch { + throw new TRPCError({ + code: "CONFLICT", + message: "Dieser Name ist leider voll belegt. Bitte einen anderen wählen.", + }); + } + } else { + discriminator = me.discriminator; + // Beim Umbenennen sicherstellen, dass die Kombination noch frei ist. + const clash = await ctx.db.query.user.findFirst({ + where: (row, { eq: rowEq, and, ne }) => + and( + rowEq(row.username, username), + rowEq(row.discriminator, discriminator), + ne(row.id, ctx.user.id), + ), + columns: { id: true }, + }); + if (clash) { + throw new TRPCError({ + code: "CONFLICT", + message: `${username}#${discriminator} ist bereits vergeben. Bitte einen anderen Namen wählen.`, + }); + } + } + + try { + await ctx.db + .update(schema.user) + .set({ username, discriminator, updatedAt: new Date() }) + .where(eq(schema.user.id, ctx.user.id)); + } catch (error) { + // Letzte Absicherung gegen die Race-Condition zwischen Prüfung und + // Schreiben: der UNIQUE-Index auf (username, discriminator) wirft bei + // einer gleichzeitig vergebenen Kombination einen DB-Fehler. + if (isUniqueViolation(error)) { + throw new TRPCError({ + code: "CONFLICT", + message: `${username}#${discriminator} ist bereits vergeben. Bitte einen anderen Namen wählen.`, + }); + } + throw error; + } + + return { ok: true, handle: formatHandle(username, discriminator) }; + }), + + // Bio und Sichtbarkeit des eigenen Community-Profils. Privacy-by-Default: + // profileVisible ist erst nach aktiver Freigabe true. + updateMyCommunityProfile: protectedProcedure + .input( + z.object({ + bio: z.string().trim().max(BIO_MAX_LENGTH).nullable().optional(), + profileVisible: z.boolean().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const patch: { bio?: string | null; profileVisible?: boolean; updatedAt: Date } = { + updatedAt: new Date(), + }; + if (input.bio !== undefined) patch.bio = input.bio?.trim() || null; + if (input.profileVisible !== undefined) patch.profileVisible = input.profileVisible; + + await ctx.db + .update(schema.user) + .set(patch) + .where(eq(schema.user.id, ctx.user.id)); + + return { ok: true }; + }), + setTheme: protectedProcedure .input(z.object({ theme: z.enum(["dark", "light"]).nullable() })) .mutation(async ({ ctx, input }) => { diff --git a/packages/db/drizzle/0019_community_profile.sql b/packages/db/drizzle/0019_community_profile.sql new file mode 100644 index 0000000..2bb61b3 --- /dev/null +++ b/packages/db/drizzle/0019_community_profile.sql @@ -0,0 +1,6 @@ +ALTER TABLE "user" ADD COLUMN "username" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "discriminator" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "bio" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "profileVisible" boolean DEFAULT false NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "user_username_discriminator_unique" ON "user" USING btree ("username","discriminator");--> statement-breakpoint +CREATE INDEX "user_username_idx" ON "user" USING btree ("username"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0019_snapshot.json b/packages/db/drizzle/meta/0019_snapshot.json new file mode 100644 index 0000000..9078968 --- /dev/null +++ b/packages/db/drizzle/meta/0019_snapshot.json @@ -0,0 +1,4686 @@ +{ + "id": "03d750c8-6e69-4d76-b9d8-6d5e90e68bb8", + "prevId": "52d2105c-e89b-4da7-8337-ff46cd3afd97", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_userId_user_id_fk": { + "name": "passkey_userId_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonatedBy": { + "name": "impersonatedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twoFactor": { + "name": "twoFactor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backupCodes": { + "name": "backupCodes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "twoFactor_userId_user_id_fk": { + "name": "twoFactor_userId_user_id_fk", + "tableFrom": "twoFactor", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "twoFactorEnabled": { + "name": "twoFactorEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "twoFactorRecoveryRequired": { + "name": "twoFactorRecoveryRequired", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "twoFactorRecoveryStartedAt": { + "name": "twoFactorRecoveryStartedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "twoFactorRecoveryStartedBy": { + "name": "twoFactorRecoveryStartedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banReason": { + "name": "banReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banExpires": { + "name": "banExpires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discriminator": { + "name": "discriminator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profileVisible": { + "name": "profileVisible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_username_discriminator_unique": { + "name": "user_username_discriminator_unique", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "discriminator", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_username_idx": { + "name": "user_username_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_settings": { + "name": "platform_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coach_principles": { + "name": "coach_principles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sortOrder": { + "name": "sortOrder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coach_recommendations": { + "name": "coach_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "coach_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'daily'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'rule'" + }, + "validFrom": { + "name": "validFrom", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "validTo": { + "name": "validTo", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "dismissedAt": { + "name": "dismissedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coach_recommendations_user_idx": { + "name": "coach_recommendations_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coach_recommendations_userId_user_id_fk": { + "name": "coach_recommendations_userId_user_id_fk", + "tableFrom": "coach_recommendations", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.health_goals": { + "name": "health_goals", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drinkMl": { + "name": "drinkMl", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2500 + }, + "calories": { + "name": "calories", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2000 + }, + "carbsG": { + "name": "carbsG", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 200 + }, + "fatG": { + "name": "fatG", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 67 + }, + "proteinG": { + "name": "proteinG", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 150 + }, + "weightKg": { + "name": "weightKg", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "fastingHours": { + "name": "fastingHours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 12 + }, + "weeklyCalories": { + "name": "weeklyCalories", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sportAddsCalories": { + "name": "sportAddsCalories", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "health_goals_userId_user_id_fk": { + "name": "health_goals_userId_user_id_fk", + "tableFrom": "health_goals", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.health_profiles": { + "name": "health_profiles", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "health_goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'healthier'" + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heightCm": { + "name": "heightCm", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "currentWeightKg": { + "name": "currentWeightKg", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "targetWeightKg": { + "name": "targetWeightKg", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "activityLevel": { + "name": "activityLevel", + "type": "activity_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'moderate'" + }, + "dietStyle": { + "name": "dietStyle", + "type": "diet_style", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'flexible'" + }, + "dietPreferences": { + "name": "dietPreferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"vegetarian\",\"vegan\",\"meat\",\"fish\"]'::jsonb" + }, + "mealsPerDay": { + "name": "mealsPerDay", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "cooksPerDay": { + "name": "cooksPerDay", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "preferredMealPrepDays": { + "name": "preferredMealPrepDays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "meatFishFrequency": { + "name": "meatFishFrequency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'flexible'" + }, + "cookingTime": { + "name": "cookingTime", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "budgetFocus": { + "name": "budgetFocus", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "excludedFoods": { + "name": "excludedFoods", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "biggestChallenge": { + "name": "biggestChallenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "medicalNote": { + "name": "medicalNote", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "health_profiles_userId_user_id_fk": { + "name": "health_profiles_userId_user_id_fk", + "tableFrom": "health_profiles", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_rules": { + "name": "reminder_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "reminder_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "intervalMinutes": { + "name": "intervalMinutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "startTime": { + "name": "startTime", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'08:00'" + }, + "endTime": { + "name": "endTime", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'20:00'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quietDays": { + "name": "quietDays", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reminder_rules_user_kind_unique": { + "name": "reminder_rules_user_kind_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reminder_rules_userId_user_id_fk": { + "name": "reminder_rules_userId_user_id_fk", + "tableFrom": "reminder_rules", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weekly_calorie_plans": { + "name": "weekly_calorie_plans", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "plannedCalories": { + "name": "plannedCalories", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "extraCalories": { + "name": "extraCalories", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weekly_calorie_plans_userId_user_id_fk": { + "name": "weekly_calorie_plans_userId_user_id_fk", + "tableFrom": "weekly_calorie_plans", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "weekly_calorie_plans_userId_date_pk": { + "name": "weekly_calorie_plans_userId_date_pk", + "columns": [ + "userId", + "date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.food_nutrition_sources": { + "name": "food_nutrition_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerFoodId": { + "name": "providerFoodId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kcalPer100g": { + "name": "kcalPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "proteinPer100g": { + "name": "proteinPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "carbsPer100g": { + "name": "carbsPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "fatPer100g": { + "name": "fatPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "servingG": { + "name": "servingG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "raw": { + "name": "raw", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "food_nutrition_sources_food_idx": { + "name": "food_nutrition_sources_food_idx", + "columns": [ + { + "expression": "foodId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "food_nutrition_sources_provider_idx": { + "name": "food_nutrition_sources_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "providerFoodId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "food_nutrition_sources_foodId_foods_id_fk": { + "name": "food_nutrition_sources_foodId_foods_id_fk", + "tableFrom": "food_nutrition_sources", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.food_portions": { + "name": "food_portions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "portion_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "gramsEquivalent": { + "name": "gramsEquivalent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "food_portions_food_idx": { + "name": "food_portions_food_idx", + "columns": [ + { + "expression": "foodId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "food_portions_foodId_foods_id_fk": { + "name": "food_portions_foodId_foods_id_fk", + "tableFrom": "food_portions", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.food_user_preferences": { + "name": "food_user_preferences", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "food_user_preferences_user_idx": { + "name": "food_user_preferences_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "food_user_preferences_food_idx": { + "name": "food_user_preferences_food_idx", + "columns": [ + { + "expression": "foodId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "food_user_preferences_userId_user_id_fk": { + "name": "food_user_preferences_userId_user_id_fk", + "tableFrom": "food_user_preferences", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "food_user_preferences_foodId_foods_id_fk": { + "name": "food_user_preferences_foodId_foods_id_fk", + "tableFrom": "food_user_preferences", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "food_user_preferences_userId_foodId_pk": { + "name": "food_user_preferences_userId_foodId_pk", + "columns": [ + "userId", + "foodId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.foods": { + "name": "foods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ownerUserId": { + "name": "ownerUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "barcode": { + "name": "barcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Sonstiges'" + }, + "kcalPer100g": { + "name": "kcalPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "proteinPer100g": { + "name": "proteinPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "carbsPer100g": { + "name": "carbsPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "fatPer100g": { + "name": "fatPer100g", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "servingG": { + "name": "servingG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'100'" + }, + "preferredUnit": { + "name": "preferredUnit", + "type": "portion_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'g'" + }, + "densityGPerMl": { + "name": "densityGPerMl", + "type": "numeric(8, 4)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "food_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "isVerified": { + "name": "isVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isCommunity": { + "name": "isCommunity", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "availableEverywhere": { + "name": "availableEverywhere", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "foods_owner_idx": { + "name": "foods_owner_idx", + "columns": [ + { + "expression": "ownerUserId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "foods_barcode_idx": { + "name": "foods_barcode_idx", + "columns": [ + { + "expression": "barcode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "foods_name_idx": { + "name": "foods_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "foods_ownerUserId_user_id_fk": { + "name": "foods_ownerUserId_user_id_fk", + "tableFrom": "foods", + "tableTo": "user", + "columnsFrom": [ + "ownerUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.food_stores": { + "name": "food_stores", + "schema": "", + "columns": { + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storeId": { + "name": "storeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departmentId": { + "name": "departmentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastPrice": { + "name": "lastPrice", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "availabilityTags": { + "name": "availabilityTags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "food_stores_store_idx": { + "name": "food_stores_store_idx", + "columns": [ + { + "expression": "storeId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "food_stores_foodId_foods_id_fk": { + "name": "food_stores_foodId_foods_id_fk", + "tableFrom": "food_stores", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "food_stores_storeId_stores_id_fk": { + "name": "food_stores_storeId_stores_id_fk", + "tableFrom": "food_stores", + "tableTo": "stores", + "columnsFrom": [ + "storeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "food_stores_departmentId_store_departments_id_fk": { + "name": "food_stores_departmentId_store_departments_id_fk", + "tableFrom": "food_stores", + "tableTo": "store_departments", + "columnsFrom": [ + "departmentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "food_stores_foodId_storeId_pk": { + "name": "food_stores_foodId_storeId_pk", + "columns": [ + "foodId", + "storeId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.store_change_requests": { + "name": "store_change_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storeId": { + "name": "storeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requestType": { + "name": "requestType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'structure_update'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedStore": { + "name": "proposedStore", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "proposedDepartments": { + "name": "proposedDepartments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reviewedBy": { + "name": "reviewedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reviewNote": { + "name": "reviewNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptedStoreId": { + "name": "acceptedStoreId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_change_requests_user_idx": { + "name": "store_change_requests_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "store_change_requests_status_idx": { + "name": "store_change_requests_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "store_change_requests_userId_user_id_fk": { + "name": "store_change_requests_userId_user_id_fk", + "tableFrom": "store_change_requests", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "store_change_requests_storeId_stores_id_fk": { + "name": "store_change_requests_storeId_stores_id_fk", + "tableFrom": "store_change_requests", + "tableTo": "stores", + "columnsFrom": [ + "storeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "store_change_requests_reviewedBy_user_id_fk": { + "name": "store_change_requests_reviewedBy_user_id_fk", + "tableFrom": "store_change_requests", + "tableTo": "user", + "columnsFrom": [ + "reviewedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "store_change_requests_acceptedStoreId_stores_id_fk": { + "name": "store_change_requests_acceptedStoreId_stores_id_fk", + "tableFrom": "store_change_requests", + "tableTo": "stores", + "columnsFrom": [ + "acceptedStoreId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.store_departments": { + "name": "store_departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "storeId": { + "name": "storeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sortOrder": { + "name": "sortOrder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "store_departments_store_idx": { + "name": "store_departments_store_idx", + "columns": [ + { + "expression": "storeId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "store_departments_storeId_stores_id_fk": { + "name": "store_departments_storeId_stores_id_fk", + "tableFrom": "store_departments", + "tableTo": "stores", + "columnsFrom": [ + "storeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.store_user_preferences": { + "name": "store_user_preferences", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storeId": { + "name": "storeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favorite": { + "name": "favorite", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_user_preferences_user_idx": { + "name": "store_user_preferences_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "store_user_preferences_userId_user_id_fk": { + "name": "store_user_preferences_userId_user_id_fk", + "tableFrom": "store_user_preferences", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "store_user_preferences_storeId_stores_id_fk": { + "name": "store_user_preferences_storeId_stores_id_fk", + "tableFrom": "store_user_preferences", + "tableTo": "stores", + "columnsFrom": [ + "storeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "store_user_preferences_userId_storeId_pk": { + "name": "store_user_preferences_userId_storeId_pk", + "columns": [ + "userId", + "storeId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stores": { + "name": "stores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ownerUserId": { + "name": "ownerUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "placeId": { + "name": "placeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric(9, 6)", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "numeric(9, 6)", + "primaryKey": false, + "notNull": false + }, + "favorite": { + "name": "favorite", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stores_ownerUserId_user_id_fk": { + "name": "stores_ownerUserId_user_id_fk", + "tableFrom": "stores", + "tableTo": "user", + "columnsFrom": [ + "ownerUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_ingredients": { + "name": "recipe_ingredients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipeId": { + "name": "recipeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountG": { + "name": "amountG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "displayAmount": { + "name": "displayAmount", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "displayUnit": { + "name": "displayUnit", + "type": "portion_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "portionId": { + "name": "portionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sortOrder": { + "name": "sortOrder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "recipe_ingredients_recipe_idx": { + "name": "recipe_ingredients_recipe_idx", + "columns": [ + { + "expression": "recipeId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_ingredients_recipeId_recipes_id_fk": { + "name": "recipe_ingredients_recipeId_recipes_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "recipes", + "columnsFrom": [ + "recipeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_ingredients_foodId_foods_id_fk": { + "name": "recipe_ingredients_foodId_foods_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "recipe_ingredients_portionId_food_portions_id_fk": { + "name": "recipe_ingredients_portionId_food_portions_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "food_portions", + "columnsFrom": [ + "portionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_ratings": { + "name": "recipe_ratings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipeId": { + "name": "recipeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_ratings_user_recipe_unique": { + "name": "recipe_ratings_user_recipe_unique", + "columns": [ + { + "expression": "recipeId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_ratings_recipeId_recipes_id_fk": { + "name": "recipe_ratings_recipeId_recipes_id_fk", + "tableFrom": "recipe_ratings", + "tableTo": "recipes", + "columnsFrom": [ + "recipeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_ratings_userId_user_id_fk": { + "name": "recipe_ratings_userId_user_id_fk", + "tableFrom": "recipe_ratings", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_submission_ingredients": { + "name": "recipe_submission_ingredients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "submissionId": { + "name": "submissionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountG": { + "name": "amountG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "displayAmount": { + "name": "displayAmount", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "displayUnit": { + "name": "displayUnit", + "type": "portion_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "portionId": { + "name": "portionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sortOrder": { + "name": "sortOrder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "recipe_submission_ingredients_submission_idx": { + "name": "recipe_submission_ingredients_submission_idx", + "columns": [ + { + "expression": "submissionId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_submission_ingredients_submissionId_recipe_submissions_id_fk": { + "name": "recipe_submission_ingredients_submissionId_recipe_submissions_id_fk", + "tableFrom": "recipe_submission_ingredients", + "tableTo": "recipe_submissions", + "columnsFrom": [ + "submissionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_submission_ingredients_foodId_foods_id_fk": { + "name": "recipe_submission_ingredients_foodId_foods_id_fk", + "tableFrom": "recipe_submission_ingredients", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "recipe_submission_ingredients_portionId_food_portions_id_fk": { + "name": "recipe_submission_ingredients_portionId_food_portions_id_fk", + "tableFrom": "recipe_submission_ingredients", + "tableTo": "food_portions", + "columnsFrom": [ + "portionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_submissions": { + "name": "recipe_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portions": { + "name": "portions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "minutes": { + "name": "minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mealPrepDays": { + "name": "mealPrepDays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewNote": { + "name": "reviewNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedBy": { + "name": "reviewedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "acceptedRecipeId": { + "name": "acceptedRecipeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_submissions_user_idx": { + "name": "recipe_submissions_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipe_submissions_status_idx": { + "name": "recipe_submissions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_submissions_userId_user_id_fk": { + "name": "recipe_submissions_userId_user_id_fk", + "tableFrom": "recipe_submissions", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_submissions_reviewedBy_user_id_fk": { + "name": "recipe_submissions_reviewedBy_user_id_fk", + "tableFrom": "recipe_submissions", + "tableTo": "user", + "columnsFrom": [ + "reviewedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "recipe_submissions_acceptedRecipeId_recipes_id_fk": { + "name": "recipe_submissions_acceptedRecipeId_recipes_id_fk", + "tableFrom": "recipe_submissions", + "tableTo": "recipes", + "columnsFrom": [ + "acceptedRecipeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipes": { + "name": "recipes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ownerUserId": { + "name": "ownerUserId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portions": { + "name": "portions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "minutes": { + "name": "minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mealPrepDays": { + "name": "mealPrepDays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "isPublic": { + "name": "isPublic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipes_owner_idx": { + "name": "recipes_owner_idx", + "columns": [ + { + "expression": "ownerUserId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipes_ownerUserId_user_id_fk": { + "name": "recipes_ownerUserId_user_id_fk", + "tableFrom": "recipes", + "tableTo": "user", + "columnsFrom": [ + "ownerUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "templateId": { + "name": "templateId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "minutes": { + "name": "minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calories": { + "name": "calories", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "activity_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_logs_user_date_idx": { + "name": "activity_logs_user_date_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_logs_source_external_unique": { + "name": "activity_logs_source_external_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "externalId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_userId_user_id_fk": { + "name": "activity_logs_userId_user_id_fk", + "tableFrom": "activity_logs", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_logs_templateId_activity_templates_id_fk": { + "name": "activity_logs_templateId_activity_templates_id_fk", + "tableFrom": "activity_logs", + "tableTo": "activity_templates", + "columnsFrom": [ + "templateId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_templates": { + "name": "activity_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ownerUserId": { + "name": "ownerUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'habit'" + }, + "defaultMinutes": { + "name": "defaultMinutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "defaultPoints": { + "name": "defaultPoints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "defaultCalories": { + "name": "defaultCalories", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_templates_owner_idx": { + "name": "activity_templates_owner_idx", + "columns": [ + { + "expression": "ownerUserId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_templates_ownerUserId_user_id_fk": { + "name": "activity_templates_ownerUserId_user_id_fk", + "tableFrom": "activity_templates", + "tableTo": "user", + "columnsFrom": [ + "ownerUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.drink_logs": { + "name": "drink_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "amountMl": { + "name": "amountMl", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "drink_logs_user_date_idx": { + "name": "drink_logs_user_date_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drink_logs_userId_user_id_fk": { + "name": "drink_logs_userId_user_id_fk", + "tableFrom": "drink_logs", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meal_logs": { + "name": "meal_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "mealType": { + "name": "mealType", + "type": "meal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipeId": { + "name": "recipeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountLabel": { + "name": "amountLabel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amountG": { + "name": "amountG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "calories": { + "name": "calories", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "proteinG": { + "name": "proteinG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "carbsG": { + "name": "carbsG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "fatG": { + "name": "fatG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meal_logs_user_date_idx": { + "name": "meal_logs_user_date_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meal_logs_userId_user_id_fk": { + "name": "meal_logs_userId_user_id_fk", + "tableFrom": "meal_logs", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meal_logs_foodId_foods_id_fk": { + "name": "meal_logs_foodId_foods_id_fk", + "tableFrom": "meal_logs", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "meal_logs_recipeId_recipes_id_fk": { + "name": "meal_logs_recipeId_recipes_id_fk", + "tableFrom": "meal_logs", + "tableTo": "recipes", + "columnsFrom": [ + "recipeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meal_plans": { + "name": "meal_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "mealType": { + "name": "mealType", + "type": "meal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipeId": { + "name": "recipeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portions": { + "name": "portions", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "plannedCalories": { + "name": "plannedCalories", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meal_plans_user_date_idx": { + "name": "meal_plans_user_date_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meal_plans_userId_user_id_fk": { + "name": "meal_plans_userId_user_id_fk", + "tableFrom": "meal_plans", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meal_plans_recipeId_recipes_id_fk": { + "name": "meal_plans_recipeId_recipes_id_fk", + "tableFrom": "meal_plans", + "tableTo": "recipes", + "columnsFrom": [ + "recipeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_logs": { + "name": "weight_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "weightKg": { + "name": "weightKg", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "weight_logs_user_date_unique": { + "name": "weight_logs_user_date_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weight_logs_userId_user_id_fk": { + "name": "weight_logs_userId_user_id_fk", + "tableFrom": "weight_logs", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_list_items": { + "name": "shopping_list_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shoppingListId": { + "name": "shoppingListId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foodId": { + "name": "foodId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Sonstiges'" + }, + "amountG": { + "name": "amountG", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "purchaseAmount": { + "name": "purchaseAmount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checked": { + "name": "checked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sortOrder": { + "name": "sortOrder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "shopping_list_items_list_idx": { + "name": "shopping_list_items_list_idx", + "columns": [ + { + "expression": "shoppingListId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shopping_list_items_shoppingListId_shopping_lists_id_fk": { + "name": "shopping_list_items_shoppingListId_shopping_lists_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "shopping_lists", + "columnsFrom": [ + "shoppingListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shopping_list_items_foodId_foods_id_fk": { + "name": "shopping_list_items_foodId_foods_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "foods", + "columnsFrom": [ + "foodId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_lists": { + "name": "shopping_lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storeId": { + "name": "storeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startsOn": { + "name": "startsOn", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "endsOn": { + "name": "endsOn", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "shopping_lists_user_idx": { + "name": "shopping_lists_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shopping_lists_userId_user_id_fk": { + "name": "shopping_lists_userId_user_id_fk", + "tableFrom": "shopping_lists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shopping_lists_storeId_stores_id_fk": { + "name": "shopping_lists_storeId_stores_id_fk", + "tableFrom": "shopping_lists", + "tableTo": "stores", + "columnsFrom": [ + "storeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.club_activities": { + "name": "club_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "hostUserId": { + "name": "hostUserId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'nationwide'" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric(9, 6)", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "numeric(9, 6)", + "primaryKey": false, + "notNull": false + }, + "locationLabel": { + "name": "locationLabel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "durationMinutes": { + "name": "durationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "club_activities_starts_at_idx": { + "name": "club_activities_starts_at_idx", + "columns": [ + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "club_activities_hostUserId_user_id_fk": { + "name": "club_activities_hostUserId_user_id_fk", + "tableFrom": "club_activities", + "tableTo": "user", + "columnsFrom": [ + "hostUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.club_activity_participants": { + "name": "club_activity_participants", + "schema": "", + "columns": { + "activityId": { + "name": "activityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joinedAt": { + "name": "joinedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "club_activity_participants_activityId_club_activities_id_fk": { + "name": "club_activity_participants_activityId_club_activities_id_fk", + "tableFrom": "club_activity_participants", + "tableTo": "club_activities", + "columnsFrom": [ + "activityId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "club_activity_participants_userId_user_id_fk": { + "name": "club_activity_participants_userId_user_id_fk", + "tableFrom": "club_activity_participants", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "club_activity_participants_activityId_userId_pk": { + "name": "club_activity_participants_activityId_userId_pk", + "columns": [ + "activityId", + "userId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.club_profiles": { + "name": "club_profiles", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bayern'" + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric(9, 6)", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "numeric(9, 6)", + "primaryKey": false, + "notNull": false + }, + "privacyMode": { + "name": "privacyMode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'state'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "club_profiles_userId_user_id_fk": { + "name": "club_profiles_userId_user_id_fk", + "tableFrom": "club_profiles", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wearable_connections": { + "name": "wearable_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "wearable_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "providerUserId": { + "name": "providerUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenEncrypted": { + "name": "accessTokenEncrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshTokenEncrypted": { + "name": "refreshTokenEncrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastSyncAt": { + "name": "lastSyncAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "syncEnabled": { + "name": "syncEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "wearable_connections_user_provider_unique": { + "name": "wearable_connections_user_provider_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wearable_connections_userId_user_id_fk": { + "name": "wearable_connections_userId_user_id_fk", + "tableFrom": "wearable_connections", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wearable_imports": { + "name": "wearable_imports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connectionId": { + "name": "connectionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "wearable_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "wearable_imports_provider_external_unique": { + "name": "wearable_imports_provider_external_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "externalId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wearable_imports_user_date_idx": { + "name": "wearable_imports_user_date_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wearable_imports_connectionId_wearable_connections_id_fk": { + "name": "wearable_imports_connectionId_wearable_connections_id_fk", + "tableFrom": "wearable_imports", + "tableTo": "wearable_connections", + "columnsFrom": [ + "connectionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "wearable_imports_userId_user_id_fk": { + "name": "wearable_imports_userId_user_id_fk", + "tableFrom": "wearable_imports", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.activity_level": { + "name": "activity_level", + "schema": "public", + "values": [ + "low", + "moderate", + "active" + ] + }, + "public.activity_source": { + "name": "activity_source", + "schema": "public", + "values": [ + "manual", + "fitbit", + "health_connect", + "system" + ] + }, + "public.coach_priority": { + "name": "coach_priority", + "schema": "public", + "values": [ + "high", + "medium", + "low" + ] + }, + "public.diet_style": { + "name": "diet_style", + "schema": "public", + "values": [ + "mixed", + "vegetarian", + "vegan", + "low_carb", + "flexible" + ] + }, + "public.food_source": { + "name": "food_source", + "schema": "public", + "values": [ + "seed", + "manual", + "openfoodfacts", + "ocr", + "community" + ] + }, + "public.health_goal": { + "name": "health_goal", + "schema": "public", + "values": [ + "lose_weight", + "maintain", + "build_muscle", + "healthier" + ] + }, + "public.meal_type": { + "name": "meal_type", + "schema": "public", + "values": [ + "breakfast", + "lunch", + "dinner", + "snack" + ] + }, + "public.portion_unit": { + "name": "portion_unit", + "schema": "public", + "values": [ + "g", + "ml", + "tsp", + "tbsp", + "piece", + "pack", + "bottle", + "cup", + "slice" + ] + }, + "public.reminder_kind": { + "name": "reminder_kind", + "schema": "public", + "values": [ + "water", + "snack", + "movement", + "meal", + "shopping", + "activity" + ] + }, + "public.wearable_provider": { + "name": "wearable_provider", + "schema": "public", + "values": [ + "fitbit", + "health_connect", + "google_fit" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 1b6cdd0..2e86286 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1780046007288, "tag": "0018_outgoing_betty_ross", "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1780134873711, + "tag": "0019_community_profile", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts index 193520e..82ccee7 100644 --- a/packages/db/src/schema/auth.ts +++ b/packages/db/src/schema/auth.ts @@ -1,26 +1,55 @@ // Better Auth erwartet genau diese Tabellen und Spaltennamen -import { boolean, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { + boolean, + index, + integer, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; -export const user = pgTable("user", { - id: text("id").primaryKey(), - name: text("name").notNull(), - email: text("email").notNull().unique(), - role: text("role").notNull().default("user"), - twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false), - twoFactorRecoveryRequired: boolean("twoFactorRecoveryRequired") - .notNull() - .default(false), - twoFactorRecoveryStartedAt: timestamp("twoFactorRecoveryStartedAt"), - twoFactorRecoveryStartedBy: text("twoFactorRecoveryStartedBy"), - banned: boolean("banned").notNull().default(false), - banReason: text("banReason"), - banExpires: timestamp("banExpires"), - emailVerified: boolean("emailVerified").notNull().default(false), - image: text("image"), - theme: text("theme"), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), -}); +export const user = pgTable( + "user", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + role: text("role").notNull().default("user"), + twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false), + twoFactorRecoveryRequired: boolean("twoFactorRecoveryRequired") + .notNull() + .default(false), + twoFactorRecoveryStartedAt: timestamp("twoFactorRecoveryStartedAt"), + twoFactorRecoveryStartedBy: text("twoFactorRecoveryStartedBy"), + banned: boolean("banned").notNull().default(false), + banReason: text("banReason"), + banExpires: timestamp("banExpires"), + emailVerified: boolean("emailVerified").notNull().default(false), + image: text("image"), + theme: text("theme"), + // Community-Profil (Battle.net-Style Handle "Username#XXXX"). + // username + discriminator sind nullable, weil der Handle erst bei + // Gelegenheit vergeben wird. Der discriminator wird einmalig bei der + // Erst-Vergabe gesetzt und bleibt danach stabil. + username: text("username"), + discriminator: text("discriminator"), + bio: text("bio"), + // Privacy-by-Default: Profil ist privat, bis der Nutzer es freigibt. + profileVisible: boolean("profileVisible").notNull().default(false), + createdAt: timestamp("createdAt").notNull().defaultNow(), + updatedAt: timestamp("updatedAt").notNull().defaultNow(), + }, + (table) => ({ + handleUnique: uniqueIndex("user_username_discriminator_unique").on( + table.username, + table.discriminator, + ), + // Index für die Handle-Erstvergabe: dort werden alle Konten mit demselben + // username geladen, um belegte Discriminatoren zu meiden (users.setMyHandle). + usernameIdx: index("user_username_idx").on(table.username), + }), +); export const session = pgTable("session", { id: text("id").primaryKey(),