import { doublePrecision, index, integer, pgEnum, pgTable, real, serial, text, timestamp, unique, uuid, varchar } from 'drizzle-orm/pg-core'; import { user } from './auth.schema'; import { availabilityValues } from '../../availability'; export const tasks = pgTable('tasks', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), currentPoints: integer('current_points').default(0), totalPoints: integer('total_points').default(1), notes: varchar('notes', { length: 8192 }), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }); export const topics = pgTable('topics', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), emoji: varchar('emoji', { length: 256 }), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }); export const tasksToTopics = pgTable('tasks_to_topics', { taskId: integer('task_id') .notNull() .references(() => tasks.id), topicId: integer('topic_id') .notNull() .references(() => topics.id) }); export const universities = pgTable('universities', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), progress: integer('progress').default(0), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }); export const certificates = pgTable('certificates', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), progress: integer('progress').default(0), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }); export const publications = pgTable('publications', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }); // Legacy ^ // New export const blogArticleStatus = pgEnum('blog_article_status', ['draft', 'published']); export const blogArticles = pgTable( 'blog_articles', { id: serial('id').primaryKey(), title: varchar('title', { length: 256 }).notNull(), slug: varchar('slug', { length: 256 }).notNull(), content: text('content').default(''), status: blogArticleStatus('status').notNull().default('draft'), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }, (table) => [unique().on(table.slug)] ); // CatchEmAll export const availability = pgEnum('availability', availabilityValues); export const pokemon = pgTable('pokemon', { id: serial('id').primaryKey(), number: integer('number').notNull(), generation: integer('generation').notNull().default(1), name: varchar('name', { length: 256 }).notNull(), type1: varchar('type1', { length: 256 }).notNull(), type2: varchar('type2', { length: 256 }), hp: integer('hp').notNull().default(0), attack: integer('attack').notNull().default(0), defense: integer('defense').notNull().default(0), specialAttack: integer('special_attack').notNull().default(0), specialDefense: integer('special_defense').notNull().default(0), speed: integer('speed').notNull().default(0) }); export const game = pgTable('game', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }).notNull(), generation: integer('generation').notNull().default(1) }); export const pokemonGameEntry = pgTable( 'pokemon_game_entry', { id: serial('id').primaryKey(), pokemonId: integer('pokemon_id') .notNull() .references(() => pokemon.id, { onDelete: 'cascade' }), gameId: integer('game_id') .notNull() .references(() => game.id, { onDelete: 'cascade' }), availability: availability('availability').notNull().default('catchable'), // optional extra detail for the tooltip, e.g. "trade with a Blue version owner" note: varchar('note', { length: 256 }) }, (table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)] ); export const userEntry = pgTable( 'user_entry', { id: serial('id').primaryKey(), pokemonGameEntryId: integer('pokemon_game_entry_id') .notNull() .references(() => pokemonGameEntry.id, { onDelete: 'cascade' }), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }) }, (table) => [unique().on(table.pokemonGameEntryId, table.userId), index().on(table.userId)] ); // which games a user has chosen to track ("rules") export const userGame = pgTable( 'user_game', { id: serial('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), gameId: integer('game_id') .notNull() .references(() => game.id, { onDelete: 'cascade' }) }, (table) => [unique().on(table.userId, table.gameId), index().on(table.userId)] ); // Animaldex export const animals = pgTable('animals', { id: uuid('id').primaryKey().defaultRandom(), species: text('species').notNull(), breed: text('breed'), animalName: text('animal_name'), description: text('description'), // AI detection stored on the animal (from the first/primary photo) aiBreedSuggestion: text('ai_breed_suggestion'), aiBreedConfidence: real('ai_breed_confidence'), // Moderation — the animal entity is moderated once. // Accepted = visible on map (as long as it has accepted sightings too). // Denied animals are hidden but not deleted. submittedAt: timestamp('submitted_at', { withTimezone: true }).notNull().defaultNow(), acceptedAt: timestamp('accepted_at', { withTimezone: true }), deniedAt: timestamp('denied_at', { withTimezone: true }) }); // The PostGIS `location geography(Point, 4326)` column is managed outside Drizzle // via a trigger in the migration — it auto-syncs from lat/lng on insert/update. // Never write to `location` directly from application code. export const sightings = pgTable('sightings', { id: uuid('id').primaryKey().defaultRandom(), animalId: uuid('animal_id') .notNull() .references(() => animals.id, { onDelete: 'cascade' }), reporterName: text('reporter_name'), seenAt: timestamp('seen_at', { withTimezone: true }).notNull(), lat: doublePrecision('lat').notNull(), lng: doublePrecision('lng').notNull(), // `location` geography column exists in DB but not mapped in Drizzle — use sql`` for spatial queries submittedAt: timestamp('submitted_at', { withTimezone: true }).notNull().defaultNow(), acceptedAt: timestamp('accepted_at', { withTimezone: true }), deniedAt: timestamp('denied_at', { withTimezone: true }) }); export const photos = pgTable('photos', { id: uuid('id').primaryKey().defaultRandom(), sightingId: uuid('sighting_id') .notNull() .references(() => sightings.id, { onDelete: 'cascade' }), r2Key: text('r2_key').notNull(), url: text('url').notNull(), sortOrder: integer('sort_order').notNull().default(0), uploadedAt: timestamp('uploaded_at', { withTimezone: true }).notNull().defaultNow() }); export type Pokemon = typeof pokemon.$inferSelect; export type Game = typeof game.$inferSelect; export type PokemonGameEntry = typeof pokemonGameEntry.$inferSelect; export type UserEntry = typeof userEntry.$inferSelect; export type UserGame = typeof userGame.$inferSelect; export type Availability = (typeof availability.enumValues)[number]; export type Animal = typeof animals.$inferSelect; export type NewAnimal = typeof animals.$inferInsert; export type Sighting = typeof sightings.$inferSelect; export type NewSighting = typeof sightings.$inferInsert; export type Photo = typeof photos.$inferSelect; // ── Helpers ─────────────────────────────────────────────────────────────────── export type ModerationStatus = 'pending' | 'accepted' | 'denied'; export function getModerationStatus( row: Pick ): ModerationStatus { if (row.deniedAt) return 'denied'; if (row.acceptedAt) return 'accepted'; return 'pending'; } export * from './auth.schema';