beeeeg update
Some checks failed
Test and deploy staging / staging (push) Has been cancelled

This commit is contained in:
2026-09-23 04:15:10 +00:00
parent 487eff5bb2
commit d0c7b6548b
135 changed files with 9531 additions and 1777 deletions

View File

@@ -0,0 +1,69 @@
import { doublePrecision, integer, pgTable, text } from 'drizzle-orm/pg-core';
import { id, timestamps, timestamptz } from './helpers';
export const animals = pgTable('animals', {
id: id(),
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: doublePrecision('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: timestamptz('submitted_at').notNull().defaultNow(),
acceptedAt: timestamptz('accepted_at'),
deniedAt: timestamptz('denied_at'),
...timestamps
});
export const sightings = pgTable('sightings', {
id: id(),
animalId: integer('animal_id')
.notNull()
.references(() => animals.id, { onDelete: 'cascade' }),
reporterName: text('reporter_name'),
seenAt: timestamptz('seen_at').notNull(),
lat: doublePrecision('lat').notNull(),
lng: doublePrecision('lng').notNull(),
submittedAt: timestamptz('submitted_at').notNull().defaultNow(),
acceptedAt: timestamptz('accepted_at'),
deniedAt: timestamptz('denied_at'),
...timestamps
});
export const photos = pgTable('photos', {
id: id(),
sightingId: integer('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: timestamptz('uploaded_at').notNull().defaultNow(),
...timestamps
});
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;
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';
}

View File

@@ -0,0 +1,78 @@
import { pgTable, text, timestamp, boolean, index, uniqueIndex } from 'drizzle-orm/pg-core';
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
role: text('role').notNull().default('user'),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').default(false).notNull(),
image: text('image'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull()
});
export const session = pgTable(
'session',
{
id: text('id').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' })
},
(table) => [index('session_userId_idx').on(table.userId)]
);
export const account = pgTable(
'account',
{
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
issuer: text('issuer').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull()
},
(table) => [
index('account_userId_idx').on(table.userId),
uniqueIndex('account_issuer_account_id_unique').on(table.issuer, table.accountId)
]
);
export const verification = pgTable(
'verification',
{
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull()
},
(table) => [index('verification_identifier_idx').on(table.identifier)]
);

View File

@@ -0,0 +1,22 @@
import { pgEnum, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { id, timestamps } from './helpers';
export const blogArticleStatus = pgEnum('blog_article_status', ['draft', 'published']);
export const blogArticles = pgTable(
'blog_articles',
{
id: id(),
title: text('title').notNull(),
slug: text('slug').notNull(),
content: text('content').notNull().default(''),
tags: text('tags').array().notNull().default([]),
status: blogArticleStatus('status').notNull().default('draft'),
...timestamps
},
(table) => [unique().on(table.slug)]
);
export type BlogArticle = typeof blogArticles.$inferSelect;
export type NewBlogArticle = typeof blogArticles.$inferInsert;
export type BlogArticleStatus = (typeof blogArticleStatus.enumValues)[number];

View File

@@ -0,0 +1,96 @@
import { index, integer, pgEnum, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { availabilityValues } from '../../../availability';
import { user } from './auth';
import { id, timestamps } from './helpers';
export const availability = pgEnum('availability', availabilityValues);
export const pokemon = pgTable('pokemon', {
id: id(),
number: integer('number').notNull().unique(),
generation: integer('generation').notNull().default(1),
name: text('name').notNull(),
type1: text('type1').notNull(),
type2: text('type2'),
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),
...timestamps
});
export const games = pgTable('games', {
id: id(),
sourceId: integer('source_id').unique(),
name: text('name').notNull(),
generation: integer('generation').notNull().default(1),
...timestamps
});
export const pokemonGameEntries = pgTable(
'pokemon_game_entries',
{
id: id(),
pokemonId: integer('pokemon_id')
.notNull()
.references(() => pokemon.id, { onDelete: 'cascade' }),
gameId: integer('game_id')
.notNull()
.references(() => games.id, { onDelete: 'cascade' }),
availability: availability('availability').notNull().default('catchable'),
// optional extra detail for the tooltip, e.g. "trade with a Blue version owner"
note: text('note'),
...timestamps
},
(table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)]
);
export const userEntries = pgTable(
'user_entries',
{
id: id(),
pokemonGameEntryId: integer('pokemon_game_entry_id')
.notNull()
.references(() => pokemonGameEntries.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
...timestamps
},
(table) => [unique().on(table.pokemonGameEntryId, table.userId), index().on(table.userId)]
);
// which games a user has chosen to track ("rules")
export const userGames = pgTable(
'user_games',
{
id: id(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
gameId: integer('game_id')
.notNull()
.references(() => games.id, { onDelete: 'cascade' }),
...timestamps
},
(table) => [unique().on(table.userId, table.gameId), index().on(table.userId)]
);
// A row distinguishes an explicit empty game selection from a new account.
export const catchEmAllSettings = pgTable('catch_em_all_settings', {
id: id(),
userId: text('user_id')
.notNull()
.unique()
.references(() => user.id, { onDelete: 'cascade' }),
...timestamps
});
export type Pokemon = typeof pokemon.$inferSelect;
export type Game = typeof games.$inferSelect;
export type PokemonGameEntry = typeof pokemonGameEntries.$inferSelect;
export type UserEntry = typeof userEntries.$inferSelect;
export type UserGame = typeof userGames.$inferSelect;
export type Availability = (typeof availability.enumValues)[number];

View File

@@ -0,0 +1,17 @@
import { serial, timestamp } from 'drizzle-orm/pg-core';
// Shared column patterns so every table is built the same way.
// Timestamps are always `timestamptz` — they represent a moment in time, not a
// wall-clock reading.
export const id = () => serial('id').primaryKey();
export const timestamptz = (name: string) => timestamp(name, { withTimezone: true });
export const timestamps = {
createdAt: timestamptz('created_at').notNull().defaultNow(),
updatedAt: timestamptz('updated_at')
.notNull()
.defaultNow()
.$onUpdate(() => new Date())
};

View File

@@ -0,0 +1,8 @@
// One file per domain. Everything is re-exported here so the rest of the app
// (and drizzle-kit / relations) only ever imports `$lib/server/db/schema`.
export * from './auth';
export * from './blog';
export * from './progress';
export * from './catch-em-all';
export * from './animaldex';
export * from './lists';

View File

@@ -0,0 +1,46 @@
import { boolean, index, integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { id, timestamps } from './helpers';
import { user } from './auth';
export const lists = pgTable('lists', {
id: id(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
isProgress: boolean('is_progress').notNull().default(false),
...timestamps
});
export const listItems = pgTable(
'list_items',
{
id: id(),
listId: integer('list_id')
.notNull()
.references(() => lists.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
pictureUrl: text('picture_url'),
description: text('description'),
...timestamps
},
(table) => [index().on(table.listId)]
);
export const listItemCompletions = pgTable(
'list_item_completions',
{
id: id(),
itemId: integer('item_id')
.notNull()
.references(() => listItems.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
...timestamps
},
(table) => [unique().on(table.itemId, table.userId), index().on(table.userId)]
);
export type List = typeof lists.$inferSelect;
export type NewList = typeof lists.$inferInsert;
export type ListItem = typeof listItems.$inferSelect;
export type NewListItem = typeof listItems.$inferInsert;

View File

@@ -0,0 +1,38 @@
import { integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { id, timestamps } from './helpers';
export const tasks = pgTable('tasks', {
id: id(),
name: text('name').notNull(),
currentPoints: integer('current_points').notNull().default(0),
totalPoints: integer('total_points').notNull().default(1),
notes: text('notes'),
...timestamps
});
export const topics = pgTable('topics', {
id: id(),
name: text('name').notNull(),
emoji: text('emoji'),
...timestamps
});
export const tasksToTopics = pgTable(
'tasks_to_topics',
{
id: id(),
taskId: integer('task_id')
.notNull()
.references(() => tasks.id, { onDelete: 'cascade' }),
topicId: integer('topic_id')
.notNull()
.references(() => topics.id, { onDelete: 'cascade' }),
...timestamps
},
(table) => [unique().on(table.taskId, table.topicId)]
);
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;
export type Topic = typeof topics.$inferSelect;
export type NewTopic = typeof topics.$inferInsert;