233 lines
7.6 KiB
TypeScript
Executable File
233 lines
7.6 KiB
TypeScript
Executable File
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 blogArticles = pgTable('blog_articles', {
|
|
id: serial('id').primaryKey(),
|
|
title: varchar('title', { length: 256 }),
|
|
slug: varchar('slug', { length: 256 }),
|
|
content: varchar('content', { length: 8192 }),
|
|
createdAt: timestamp('created_at').defaultNow(),
|
|
updatedAt: timestamp('updated_at').defaultNow()
|
|
});
|
|
|
|
// 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<Animal | Sighting, 'acceptedAt' | 'deniedAt'>
|
|
): ModerationStatus {
|
|
if (row.deniedAt) return 'denied';
|
|
if (row.acceptedAt) return 'accepted';
|
|
return 'pending';
|
|
}
|
|
|
|
export * from './auth.schema';
|