This commit is contained in:
@@ -2,6 +2,27 @@ import { defineRelations } from 'drizzle-orm';
|
||||
import * as schema from './schema';
|
||||
|
||||
export const relations = defineRelations(schema, (r) => ({
|
||||
// ── Auth ──────────────────────────────────────
|
||||
user: {
|
||||
sessions: r.many.session(),
|
||||
accounts: r.many.account(),
|
||||
entries: r.many.userEntries(),
|
||||
games: r.many.userGames(),
|
||||
listCompletions: r.many.listItemCompletions()
|
||||
},
|
||||
session: {
|
||||
user: r.one.user({
|
||||
from: r.session.userId,
|
||||
to: r.user.id
|
||||
})
|
||||
},
|
||||
account: {
|
||||
user: r.one.user({
|
||||
from: r.account.userId,
|
||||
to: r.user.id
|
||||
})
|
||||
},
|
||||
|
||||
// ── Progress ──────────────────────────────────
|
||||
tasks: {
|
||||
tasksToTopics: r.many.tasksToTopics()
|
||||
@@ -21,61 +42,41 @@ export const relations = defineRelations(schema, (r) => ({
|
||||
},
|
||||
|
||||
// ── CatchEmAll ────────────────────────────────
|
||||
game: {
|
||||
entries: r.many.pokemonGameEntry()
|
||||
games: {
|
||||
entries: r.many.pokemonGameEntries()
|
||||
},
|
||||
pokemon: {
|
||||
entry: r.many.pokemonGameEntry()
|
||||
entry: r.many.pokemonGameEntries()
|
||||
},
|
||||
pokemonGameEntry: {
|
||||
game: r.one.game({
|
||||
from: r.pokemonGameEntry.gameId,
|
||||
to: r.game.id
|
||||
pokemonGameEntries: {
|
||||
game: r.one.games({
|
||||
from: r.pokemonGameEntries.gameId,
|
||||
to: r.games.id
|
||||
}),
|
||||
pokemon: r.one.pokemon({
|
||||
from: r.pokemonGameEntry.pokemonId,
|
||||
from: r.pokemonGameEntries.pokemonId,
|
||||
to: r.pokemon.id
|
||||
}),
|
||||
userEntries: r.many.userEntry()
|
||||
userEntries: r.many.userEntries()
|
||||
},
|
||||
userEntry: {
|
||||
pokemonGameEntry: r.one.pokemonGameEntry({
|
||||
from: r.userEntry.pokemonGameEntryId,
|
||||
to: r.pokemonGameEntry.id
|
||||
userEntries: {
|
||||
pokemonGameEntry: r.one.pokemonGameEntries({
|
||||
from: r.userEntries.pokemonGameEntryId,
|
||||
to: r.pokemonGameEntries.id
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.userEntry.userId,
|
||||
from: r.userEntries.userId,
|
||||
to: r.user.id
|
||||
})
|
||||
},
|
||||
userGame: {
|
||||
userGames: {
|
||||
user: r.one.user({
|
||||
from: r.userGame.userId,
|
||||
from: r.userGames.userId,
|
||||
to: r.user.id
|
||||
}),
|
||||
game: r.one.game({
|
||||
from: r.userGame.gameId,
|
||||
to: r.game.id
|
||||
})
|
||||
},
|
||||
|
||||
// ── Auth ──────────────────────────────────────
|
||||
user: {
|
||||
sessions: r.many.session(),
|
||||
accounts: r.many.account(),
|
||||
entries: r.many.userEntry(),
|
||||
games: r.many.userGame()
|
||||
},
|
||||
session: {
|
||||
user: r.one.user({
|
||||
from: r.session.userId,
|
||||
to: r.user.id
|
||||
})
|
||||
},
|
||||
account: {
|
||||
user: r.one.user({
|
||||
from: r.account.userId,
|
||||
to: r.user.id
|
||||
game: r.one.games({
|
||||
from: r.userGames.gameId,
|
||||
to: r.games.id
|
||||
})
|
||||
},
|
||||
|
||||
@@ -95,5 +96,27 @@ export const relations = defineRelations(schema, (r) => ({
|
||||
from: r.photos.sightingId,
|
||||
to: r.sightings.id
|
||||
})
|
||||
},
|
||||
|
||||
// ── Lists ─────────────────────────────────────
|
||||
lists: {
|
||||
items: r.many.listItems()
|
||||
},
|
||||
listItems: {
|
||||
list: r.one.lists({
|
||||
from: r.listItems.listId,
|
||||
to: r.lists.id
|
||||
}),
|
||||
completions: r.many.listItemCompletions()
|
||||
},
|
||||
listItemCompletions: {
|
||||
item: r.one.listItems({
|
||||
from: r.listItemCompletions.itemId,
|
||||
to: r.listItems.id
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.listItemCompletions.userId,
|
||||
to: r.user.id
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
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 })
|
||||
});
|
||||
|
||||
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(),
|
||||
|
||||
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';
|
||||
69
src/lib/server/db/schema/animaldex.ts
Normal file
69
src/lib/server/db/schema/animaldex.ts
Normal 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';
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';
|
||||
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').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull()
|
||||
@@ -17,10 +18,10 @@ export const session = pgTable(
|
||||
'session',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
@@ -37,6 +38,7 @@ export const account = pgTable(
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
issuer: text('issuer').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
@@ -44,16 +46,19 @@ export const account = pgTable(
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
||||
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').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
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)]
|
||||
(table) => [
|
||||
index('account_userId_idx').on(table.userId),
|
||||
uniqueIndex('account_issuer_account_id_unique').on(table.issuer, table.accountId)
|
||||
]
|
||||
);
|
||||
|
||||
export const verification = pgTable(
|
||||
@@ -62,9 +67,9 @@ export const verification = pgTable(
|
||||
id: text('id').primaryKey(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
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()
|
||||
22
src/lib/server/db/schema/blog.ts
Normal file
22
src/lib/server/db/schema/blog.ts
Normal 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];
|
||||
96
src/lib/server/db/schema/catch-em-all.ts
Normal file
96
src/lib/server/db/schema/catch-em-all.ts
Normal 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];
|
||||
17
src/lib/server/db/schema/helpers.ts
Normal file
17
src/lib/server/db/schema/helpers.ts
Normal 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())
|
||||
};
|
||||
8
src/lib/server/db/schema/index.ts
Normal file
8
src/lib/server/db/schema/index.ts
Normal 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';
|
||||
46
src/lib/server/db/schema/lists.ts
Normal file
46
src/lib/server/db/schema/lists.ts
Normal 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;
|
||||
38
src/lib/server/db/schema/progress.ts
Normal file
38
src/lib/server/db/schema/progress.ts
Normal 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;
|
||||
Reference in New Issue
Block a user