db: Drizzle schemas on domain Postgres schemas, baseline migration, reorg SQL

- pgSchema tables for auth, money, house, ref, games, cs, animaldex, site, meta
  (lists excluded via schemaFilter; runtime list tables are managed by meta.*)
- drizzle/ baseline matches the reorged DB exactly (catalog diff = 0 on rehearsal)
- db/reorg: one-time SQL that moves the flat public schema (rehearsed on a prod copy)
- scripts/db-migrate.ts: migrator with --init to record the baseline and a guard
  against running the baseline on a non-empty, unrecorded database
This commit is contained in:
2026-10-23 22:03:42 +00:00
parent f94e3dc8ac
commit 64fb8df262
29 changed files with 12507 additions and 189 deletions

View File

@@ -7,8 +7,7 @@ export const relations = defineRelations(schema, (r) => ({
sessions: r.many.session(),
accounts: r.many.account(),
entries: r.many.userEntries(),
games: r.many.userGames(),
listCompletions: r.many.listItemCompletions()
games: r.many.userGames()
},
session: {
user: r.one.user({
@@ -138,25 +137,14 @@ export const relations = defineRelations(schema, (r) => ({
})
},
// ── Lists ─────────────────────────────────────
lists: {
items: r.many.listItems()
// ── Meta registry (lists live in schema `lists`, see src/lib/server/lists.ts)
metaTables: {
columns: r.many.metaColumns()
},
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
metaColumns: {
table: r.one.metaTables({
from: r.metaColumns.tableId,
to: r.metaTables.id
})
}
}));

View File

@@ -1,7 +1,8 @@
import { doublePrecision, integer, pgTable, text } from 'drizzle-orm/pg-core';
import { doublePrecision, integer, text } from 'drizzle-orm/pg-core';
import { animaldexSchema } from './schemas';
import { id, timestamps, timestamptz } from './helpers';
export const animals = pgTable('animals', {
export const animals = animaldexSchema.table('animals', {
id: id(),
species: text('species').notNull(),
@@ -22,7 +23,7 @@ export const animals = pgTable('animals', {
...timestamps
});
export const sightings = pgTable('sightings', {
export const sightings = animaldexSchema.table('sightings', {
id: id(),
animalId: integer('animal_id')
.notNull()
@@ -40,7 +41,7 @@ export const sightings = pgTable('sightings', {
...timestamps
});
export const photos = pgTable('photos', {
export const photos = animaldexSchema.table('sighting_photos', {
id: id(),
sightingId: integer('sighting_id')
.notNull()

View File

@@ -1,6 +1,7 @@
import { pgTable, text, timestamp, boolean, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { text, timestamp, boolean, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { authSchema } from './schemas';
export const user = pgTable('user', {
export const user = authSchema.table('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
role: text('role').notNull().default('user'),
@@ -14,7 +15,7 @@ export const user = pgTable('user', {
.notNull()
});
export const session = pgTable(
export const session = authSchema.table(
'session',
{
id: text('id').primaryKey(),
@@ -33,7 +34,7 @@ export const session = pgTable(
(table) => [index('session_userId_idx').on(table.userId)]
);
export const account = pgTable(
export const account = authSchema.table(
'account',
{
id: text('id').primaryKey(),
@@ -61,7 +62,7 @@ export const account = pgTable(
]
);
export const verification = pgTable(
export const verification = authSchema.table(
'verification',
{
id: text('id').primaryKey(),

View File

@@ -1,9 +1,10 @@
import { pgEnum, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { text, unique } from 'drizzle-orm/pg-core';
import { siteSchema } from './schemas';
import { id, timestamps } from './helpers';
export const blogArticleStatus = pgEnum('blog_article_status', ['draft', 'published']);
export const blogArticleStatus = siteSchema.enum('blog_article_status', ['draft', 'published']);
export const blogArticles = pgTable(
export const blogArticles = siteSchema.table(
'blog_articles',
{
id: id(),

View File

@@ -1,16 +1,17 @@
import { boolean, index, integer, pgEnum, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { boolean, index, integer, text, unique } from 'drizzle-orm/pg-core';
import { gamesSchema, refSchema } from './schemas';
import { availabilityValues } from '../../../availability';
import { user } from './auth';
import { id, timestamps } from './helpers';
export const availability = pgEnum('availability', availabilityValues);
export const availability = refSchema.enum('availability', availabilityValues);
// Where a catalog row came from. Seeding replaces `pokeapi` and `derived` rows
// but never touches `manual` ones, which are admin corrections.
export const catalogSources = ['pokeapi', 'derived', 'manual'] as const;
const source = () => text('source', { enum: catalogSources }).notNull().default('pokeapi');
export const pokemon = pgTable('pokemon', {
export const pokemon = refSchema.table('pokemon', {
id: id(),
number: integer('number').notNull().unique(),
generation: integer('generation').notNull().default(1),
@@ -32,7 +33,7 @@ export const pokemon = pgTable('pokemon', {
...timestamps
});
export const games = pgTable('games', {
export const games = refSchema.table('pokemon_games', {
id: id(),
sourceId: integer('source_id').unique(),
slug: text('slug').unique(),
@@ -43,7 +44,7 @@ export const games = pgTable('games', {
...timestamps
});
export const pokemonGameEntries = pgTable(
export const pokemonGameEntries = refSchema.table(
'pokemon_game_entries',
{
id: id(),
@@ -62,7 +63,7 @@ export const pokemonGameEntries = pgTable(
(table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)]
);
export const pokemonLocations = pgTable('pokemon_locations', {
export const pokemonLocations = refSchema.table('pokemon_locations', {
id: id(),
sourceId: integer('source_id').unique(),
slug: text('slug').notNull(),
@@ -76,7 +77,7 @@ export const pokemonLocations = pgTable('pokemon_locations', {
...timestamps
});
export const pokemonEncounters = pgTable(
export const pokemonEncounters = refSchema.table(
'pokemon_encounters',
{
id: id(),
@@ -106,7 +107,7 @@ export const pokemonEncounters = pgTable(
// The method that applies in each game, e.g. Magneton → Magnezone needs Mt.
// Coronet in Platinum but a Thunder Stone in Sword.
export const pokemonEvolutions = pgTable(
export const pokemonEvolutions = refSchema.table(
'pokemon_evolutions',
{
id: id(),
@@ -129,8 +130,8 @@ export const pokemonEvolutions = pgTable(
]
);
export const userEntries = pgTable(
'user_entries',
export const userEntries = gamesSchema.table(
'catch_em_all_entries',
{
id: id(),
pokemonGameEntryId: integer('pokemon_game_entry_id')
@@ -145,8 +146,8 @@ export const userEntries = pgTable(
);
// which games a user has chosen to track ("rules")
export const userGames = pgTable(
'user_games',
export const userGames = gamesSchema.table(
'catch_em_all_games',
{
id: id(),
userId: text('user_id')
@@ -161,7 +162,7 @@ export const userGames = pgTable(
);
// A row distinguishes an explicit empty game selection from a new account.
export const catchEmAllSettings = pgTable('catch_em_all_settings', {
export const catchEmAllSettings = gamesSchema.table('catch_em_all_settings', {
id: id(),
userId: text('user_id')
.notNull()

View File

@@ -1,9 +1,18 @@
import { index, integer, pgTable, text, timestamp, uniqueIndex, bigint } from 'drizzle-orm/pg-core';
import {
index,
integer,
primaryKey,
text,
timestamp,
uniqueIndex,
bigint
} from 'drizzle-orm/pg-core';
import { csSchema } from './schemas';
import { id } from './helpers';
// A Steam market hash name identifies a listing, including wear and special variants.
export const csMarketItems = pgTable(
'cs_market_items',
export const csMarketItems = csSchema.table(
'items',
{
id: id(),
marketHashName: text('market_hash_name').notNull(),
@@ -14,14 +23,14 @@ export const csMarketItems = pgTable(
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [
uniqueIndex('cs_market_items_hash_idx').on(table.marketHashName),
index('cs_market_items_name_idx').on(table.name)
uniqueIndex('items_hash_idx').on(table.marketHashName),
index('items_name_idx').on(table.name)
]
);
// Keep source observations intact. Aggregation happens only when the chart is requested.
export const csPriceObservations = pgTable(
'cs_price_observations',
export const csPriceObservations = csSchema.table(
'price_observations',
{
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
itemId: integer('item_id')
@@ -36,25 +45,25 @@ export const csPriceObservations = pgTable(
importedAt: timestamp('imported_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => [
uniqueIndex('cs_price_observations_unique_idx').on(
uniqueIndex('price_observations_unique_idx').on(
table.itemId,
table.source,
table.currency,
table.observedAt,
table.kind
),
index('cs_price_observations_lookup_idx').on(
index('price_observations_lookup_idx').on(
table.itemId,
table.source,
table.currency,
table.observedAt
),
index('cs_price_observations_time_idx').on(table.observedAt)
index('price_observations_time_idx').on(table.observedAt)
]
);
export const csItemDimensions = pgTable(
'cs_item_dimensions',
export const csItemDimensions = csSchema.table(
'item_dimensions',
{
itemId: integer('item_id')
.notNull()
@@ -63,19 +72,19 @@ export const csItemDimensions = pgTable(
value: text('value').notNull()
},
(table) => [
uniqueIndex('cs_item_dimensions_unique_idx').on(table.itemId, table.key),
index('cs_item_dimensions_filter_idx').on(table.key, table.value, table.itemId)
primaryKey({ name: 'item_dimensions_pkey', columns: [table.itemId, table.key] }),
index('item_dimensions_filter_idx').on(table.key, table.value, table.itemId)
]
);
export const csPriceStreams = pgTable(
'cs_price_streams',
export const csPriceStreams = csSchema.table(
'price_streams',
{
source: text('source').notNull(),
currency: text('currency').notNull(),
kind: text('kind').notNull()
},
(table) => [
uniqueIndex('cs_price_streams_unique_idx').on(table.source, table.currency, table.kind)
primaryKey({ name: 'price_streams_pkey', columns: [table.source, table.currency, table.kind] })
]
);

View File

@@ -1,11 +1,12 @@
import { boolean, index, integer, jsonb, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { boolean, index, integer, jsonb, text, unique } from 'drizzle-orm/pg-core';
import { gamesSchema, refSchema } from './schemas';
import type { MasteryRules } from '../../../game-tracker/mastery';
import { user } from './auth';
import { id, timestamps } from './helpers';
// Checklist-style games (Call of Duty and future franchises). Pokémon keeps its
// own species × game model in catch-em-all.ts.
export const trackerGames = pgTable('tracker_games', {
export const trackerGames = refSchema.table('tracker_games', {
id: id(),
slug: text('slug').notNull().unique(),
franchise: text('franchise').notNull(),
@@ -17,7 +18,7 @@ export const trackerGames = pgTable('tracker_games', {
...timestamps
});
export const trackerItems = pgTable(
export const trackerItems = refSchema.table(
'tracker_items',
{
id: id(),
@@ -41,7 +42,7 @@ export const trackerItems = pgTable(
// One row per completed milestone: `done` for achievements, or a weapon
// milestone key such as `gold` or `maxLevel`. Derived tiers are never stored.
export const trackerProgress = pgTable(
export const trackerProgress = gamesSchema.table(
'tracker_progress',
{
id: id(),

View File

@@ -6,16 +6,16 @@ import {
index,
integer,
jsonb,
pgTable,
primaryKey,
text,
uniqueIndex
} from 'drizzle-orm/pg-core';
import { houseSchema } from './schemas';
import { sql } from 'drizzle-orm';
import { id, timestamptz } from './helpers';
// Created by scripts/house-tracker-migration.sql; keep both in sync.
export const houseListings = pgTable(
'house_listings',
export const houseListings = houseSchema.table(
'listings',
{
id: id(),
source: text('source').notNull(),
@@ -58,21 +58,21 @@ export const houseListings = pgTable(
notes: text('notes')
},
(table) => [
check('house_listings_offer_check', sql`offer IN ('rent', 'buy')`),
check('house_listings_price_eur_check', sql`price_eur IS NULL OR price_eur >= 0`),
uniqueIndex('house_listings_source_idx').on(table.source, table.sourceId),
index('house_listings_active_idx')
.on(table.offer, table.firstSeenAt.desc())
check('listings_offer_check', sql`offer IN ('rent', 'buy')`),
check('listings_price_eur_check', sql`price_eur IS NULL OR price_eur >= 0`),
uniqueIndex('listings_source_idx').on(table.source, table.sourceId),
index('listings_active_idx')
.on(table.offer, table.firstSeenAt.desc().nullsFirst())
.where(sql`removed_at IS NULL`),
index('house_listings_city_idx').on(table.source, table.offer, table.citySlug),
index('house_listings_details_idx')
.on(table.firstSeenAt.desc())
index('listings_city_idx').on(table.source, table.offer, table.citySlug),
index('listings_details_idx')
.on(table.firstSeenAt.desc().nullsFirst())
.where(sql`detail_fetched_at IS NULL`)
]
);
export const housePriceHistory = pgTable(
'house_price_history',
export const housePriceHistory = houseSchema.table(
'price_history',
{
listingId: integer('listing_id')
.notNull()
@@ -80,10 +80,12 @@ export const housePriceHistory = pgTable(
observedAt: timestamptz('observed_at').notNull().defaultNow(),
priceEur: integer('price_eur').notNull()
},
(table) => [index('house_price_history_listing_idx').on(table.listingId, table.observedAt)]
(table) => [
primaryKey({ name: 'price_history_pkey', columns: [table.listingId, table.observedAt] })
]
);
export const houseSearches = pgTable('house_searches', {
export const houseSearches = houseSchema.table('searches', {
searchUrl: text('search_url').primaryKey(),
source: text('source').notNull(),
lane: text('lane').notNull(),
@@ -92,8 +94,8 @@ export const houseSearches = pgTable('house_searches', {
lastFound: integer('last_found').notNull().default(0)
});
export const houseWorkerRuns = pgTable(
'house_worker_runs',
export const houseWorkerRuns = houseSchema.table(
'worker_runs',
{
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
source: text('source').notNull(),
@@ -106,11 +108,11 @@ export const houseWorkerRuns = pgTable(
message: text('message'),
createdAt: timestamptz('created_at').notNull().defaultNow()
},
(table) => [index('house_worker_runs_created_idx').on(table.createdAt.desc())]
(table) => [index('worker_runs_created_idx').on(table.createdAt.desc().nullsFirst())]
);
export const houseAlertRules = pgTable(
'house_alert_rules',
export const houseAlertRules = houseSchema.table(
'alert_rules',
{
id: id(),
name: text('name').notNull(),
@@ -122,7 +124,11 @@ export const houseAlertRules = pgTable(
minBedrooms: integer('min_bedrooms'),
cities: text('cities').array().notNull().default([]),
propertyTypes: text('property_types').array().notNull().default([]),
createdAt: timestamptz('created_at').notNull().defaultNow()
createdAt: timestamptz('created_at').notNull().defaultNow(),
updatedAt: timestamptz('updated_at')
.notNull()
.defaultNow()
.$onUpdate(() => new Date())
},
() => [check('house_alert_rules_offer_check', sql`offer IN ('rent', 'buy')`)]
() => [check('alert_rules_offer_check', sql`offer IN ('rent', 'buy')`)]
);

View File

@@ -7,7 +7,8 @@ export * from './catch-em-all';
export * from './game-tracker';
export * from './tracker-journal';
export * from './animaldex';
export * from './lists';
export * from './lists'; // meta registry; the lists themselves are runtime tables
export * from './schemas';
export * from './money-tracker';
export * from './cs-prices';
export * from './house-tracker';

View File

@@ -1,47 +1,92 @@
import { boolean, index, integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { id, timestamps } from './helpers';
import { user } from './auth';
import {
boolean,
check,
index,
integer,
jsonb,
text,
timestamp,
unique
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { metaSchema } from './schemas';
export const lists = pgTable('lists', {
id: id(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
pictureUrl: text('picture_url'),
isProgress: boolean('is_progress').notNull().default(false),
...timestamps
});
// Registry of GUI-visible tables. Runtime lists live in schema `lists` (one real
// table per list, created through meta.create_list()); that schema is excluded
// from drizzle-kit via schemaFilter. Structure truth is pg_catalog; these rows are
// presentation. See src/lib/server/lists.ts for the list engine.
const ts = (name: string) => timestamp(name, { withTimezone: true });
export const listItems = pgTable(
'list_items',
export const metaTables = metaSchema.table(
'tables',
{
id: id(),
listId: integer('list_id')
.notNull()
.references(() => lists.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
pictureUrl: text('picture_url'),
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
schemaName: text('schema_name').notNull(),
tableName: text('table_name').notNull(),
slug: text('slug').notNull().unique('tables_slug_key'),
displayName: text('display_name').notNull(),
description: text('description'),
...timestamps
icon: text('icon'),
pictureUrl: text('picture_url'),
kind: text('kind', { enum: ['list', 'core', 'ref'] }).notNull(),
editable: boolean('editable').notNull().default(true),
hasDone: boolean('has_done').notNull().default(false),
defaultSort: text('default_sort'),
sortOrder: integer('sort_order').notNull().default(0),
archivedAt: ts('archived_at'),
createdAt: ts('created_at').notNull().defaultNow(),
updatedAt: ts('updated_at').notNull().defaultNow()
},
(table) => [index().on(table.listId)]
(t) => [
unique('tables_schema_name_table_name_key').on(t.schemaName, t.tableName),
check('tables_slug_check', sql`slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'`),
check('tables_kind_check', sql`kind IN ('list','core','ref')`)
]
);
export const listItemCompletions = pgTable(
'list_item_completions',
export const uiTypes = [
'text',
'longtext',
'url',
'image',
'number',
'cents',
'date',
'datetime',
'bool',
'select',
'multiselect',
'fk',
'json'
] as const;
export type UiType = (typeof uiTypes)[number];
export const metaColumns = metaSchema.table(
'columns',
{
id: id(),
itemId: integer('item_id')
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
tableId: integer('table_id')
.notNull()
.references(() => listItems.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
...timestamps
.references(() => metaTables.id, { onDelete: 'cascade' }),
columnName: text('column_name').notNull(),
label: text('label'),
uiType: text('ui_type', { enum: uiTypes }).notNull(),
options: jsonb('options').$type<{ values?: string[] }>().notNull().default({}),
position: integer('position').notNull().default(0),
hidden: boolean('hidden').notNull().default(false),
helpText: text('help_text'),
createdAt: ts('created_at').notNull().defaultNow(),
updatedAt: ts('updated_at').notNull().defaultNow()
},
(table) => [unique().on(table.itemId, table.userId), index().on(table.userId)]
(t) => [
unique('columns_table_id_column_name_key').on(t.tableId, t.columnName),
index('columns_table_id_index').on(t.tableId),
check(
'columns_ui_type_check',
sql`ui_type IN ('text','longtext','url','image','number','cents','date','datetime','bool','select','multiselect','fk','json')`
)
]
);
export type List = typeof lists.$inferSelect;
export type NewList = typeof lists.$inferInsert;
export type ListItem = typeof listItems.$inferSelect;
export type NewListItem = typeof listItems.$inferInsert;
export type MetaTable = typeof metaTables.$inferSelect;
export type MetaColumn = typeof metaColumns.$inferSelect;

View File

@@ -1,9 +1,11 @@
import { date, index, integer, numeric, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { boolean, check, date, index, integer, numeric, text, unique } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { moneySchema } from './schemas';
import { id, timestamps } from './helpers';
import { user } from './auth';
export const moneyAccounts = pgTable(
'money_accounts',
export const moneyAccounts = moneySchema.table(
'accounts',
{
id: id(),
userId: text('user_id')
@@ -15,11 +17,15 @@ export const moneyAccounts = pgTable(
color: text('color'),
...timestamps
},
(table) => [unique().on(table.userId, table.name), index().on(table.userId)]
(table) => [
unique().on(table.userId, table.name),
index().on(table.userId),
check('accounts_kind_check', sql`kind IN ('spendable','investment')`)
]
);
export const moneyTransactions = pgTable(
'money_transactions',
export const moneyTransactions = moneySchema.table(
'transactions',
{
id: id(),
userId: text('user_id')
@@ -36,8 +42,8 @@ export const moneyTransactions = pgTable(
(table) => [index().on(table.userId, table.date, table.id), index().on(table.accountId)]
);
export const moneyRecurring = pgTable(
'money_recurring',
export const moneyRecurring = moneySchema.table(
'recurring',
{
id: id(),
userId: text('user_id')
@@ -51,11 +57,14 @@ export const moneyRecurring = pgTable(
accountId: integer('account_id').references(() => moneyAccounts.id, { onDelete: 'set null' }),
...timestamps
},
(table) => [index().on(table.userId)]
(table) => [
index().on(table.userId),
check('recurring_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`)
]
);
export const moneyForecasts = pgTable(
'money_forecasts',
export const moneyForecasts = moneySchema.table(
'forecasts',
{
id: id(),
userId: text('user_id')
@@ -70,8 +79,8 @@ export const moneyForecasts = pgTable(
(table) => [index().on(table.userId, table.date)]
);
export const moneyUtilitySettings = pgTable(
'money_utility_settings',
export const moneyUtilitySettings = moneySchema.table(
'utility_settings',
{
id: id(),
userId: text('user_id')
@@ -83,11 +92,14 @@ export const moneyUtilitySettings = pgTable(
billingStart: date('billing_start', { mode: 'string' }),
...timestamps
},
(table) => [unique().on(table.userId, table.kind)]
(table) => [
unique().on(table.userId, table.kind),
check('utility_settings_kind_check', sql`kind IN ('electricity','gas','water')`)
]
);
export const moneyUtilityReadings = pgTable(
'money_utility_readings',
export const moneyUtilityReadings = moneySchema.table(
'utility_readings',
{
id: id(),
userId: text('user_id')
@@ -98,11 +110,14 @@ export const moneyUtilityReadings = pgTable(
reading: numeric('reading', { precision: 12, scale: 3 }).notNull(),
...timestamps
},
(table) => [index().on(table.userId, table.kind, table.date)]
(table) => [
index().on(table.userId, table.kind, table.date),
check('utility_readings_kind_check', sql`kind IN ('electricity','gas','water')`)
]
);
export const moneyWishlist = pgTable(
'money_wishlist',
export const moneyWishlist = moneySchema.table(
'wishlist',
{
id: id(),
userId: text('user_id')
@@ -117,8 +132,8 @@ export const moneyWishlist = pgTable(
(table) => [index().on(table.userId)]
);
export const moneyProjectionSettings = pgTable(
'money_projection_settings',
export const moneyProjectionSettings = moneySchema.table(
'projection_settings',
{
id: id(),
userId: text('user_id')
@@ -132,16 +147,22 @@ export const moneyProjectionSettings = pgTable(
.notNull()
.default('5.00'),
contributionMode: text('contribution_mode').notNull().default('excess'),
includeRecurring: integer('include_recurring').notNull().default(1),
includeRecurring: boolean('include_recurring').notNull().default(true),
fixedMonthlyCents: integer('fixed_monthly_cents').notNull().default(0),
endDate: date('end_date', { mode: 'string' }),
...timestamps
},
(table) => [unique().on(table.userId)]
(table) => [
unique().on(table.userId),
check(
'projection_settings_contribution_mode_check',
sql`contribution_mode IN ('excess','fixed')`
)
]
);
export const moneyInsurances = pgTable(
'money_insurances',
export const moneyInsurances = moneySchema.table(
'insurances',
{
id: id(),
userId: text('user_id')
@@ -154,5 +175,8 @@ export const moneyInsurances = pgTable(
cadence: text('cadence').notNull().default('monthly'),
...timestamps
},
(table) => [index().on(table.userId)]
(table) => [
index().on(table.userId),
check('insurances_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`)
]
);

View File

@@ -1,7 +1,8 @@
import { integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
import { integer, text, unique } from 'drizzle-orm/pg-core';
import { siteSchema } from './schemas';
import { id, timestamps } from './helpers';
export const tasks = pgTable('tasks', {
export const tasks = siteSchema.table('tasks', {
id: id(),
name: text('name').notNull(),
currentPoints: integer('current_points').notNull().default(0),
@@ -10,15 +11,15 @@ export const tasks = pgTable('tasks', {
...timestamps
});
export const topics = pgTable('topics', {
export const topics = siteSchema.table('topics', {
id: id(),
name: text('name').notNull(),
emoji: text('emoji'),
...timestamps
});
export const tasksToTopics = pgTable(
'tasks_to_topics',
export const tasksToTopics = siteSchema.table(
'task_topics',
{
id: id(),
taskId: integer('task_id')

View File

@@ -0,0 +1,14 @@
import { pgSchema } from 'drizzle-orm/pg-core';
// One Postgres schema per domain (see ~/dev/setup/db/PLAN.md). `lists`, `legacy`,
// `energy` and `hardware` are deliberately not modelled here: lists are created at
// runtime through meta.create_list(), and energy/hardware belong to other tools.
export const authSchema = pgSchema('auth');
export const moneySchema = pgSchema('money');
export const houseSchema = pgSchema('house');
export const refSchema = pgSchema('ref');
export const gamesSchema = pgSchema('games');
export const csSchema = pgSchema('cs');
export const animaldexSchema = pgSchema('animaldex');
export const siteSchema = pgSchema('site');
export const metaSchema = pgSchema('meta');

View File

@@ -2,20 +2,21 @@ import {
boolean,
check,
date,
numeric,
index,
integer,
jsonb,
pgTable,
text,
unique
} from 'drizzle-orm/pg-core';
import { gamesSchema, refSchema } from './schemas';
import { sql } from 'drizzle-orm';
import { user } from './auth';
import { games, pokemon } from './catch-em-all';
import { trackerGames, trackerItems } from './game-tracker';
import { id, timestamps } from './helpers';
export const pokemonResearch = pgTable('pokemon_research', {
export const pokemonResearch = refSchema.table('pokemon_research', {
pokemonId: integer('pokemon_id')
.primaryKey()
.references(() => pokemon.id, { onDelete: 'cascade' }),
@@ -36,7 +37,7 @@ export const pokemonResearch = pgTable('pokemon_research', {
.notNull()
});
export const pokemonDexNumbers = pgTable(
export const pokemonDexNumbers = refSchema.table(
'pokemon_dex_numbers',
{
id: id(),
@@ -53,7 +54,7 @@ export const pokemonDexNumbers = pgTable(
(t) => [unique().on(t.pokemonId, t.gameId, t.dex), index().on(t.gameId)]
);
export const pokemonMoves = pgTable('pokemon_moves', {
export const pokemonMoves = refSchema.table('pokemon_moves', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
type: text('type').notNull(),
@@ -65,7 +66,7 @@ export const pokemonMoves = pgTable('pokemon_moves', {
target: text('target').notNull()
});
export const pokemonLearnsets = pgTable(
export const pokemonLearnsets = refSchema.table(
'pokemon_learnsets',
{
id: id(),
@@ -78,7 +79,7 @@ export const pokemonLearnsets = pgTable(
(t) => [unique().on(t.pokemonId, t.versionGroup)]
);
export const pokemonGameResearch = pgTable('pokemon_game_research', {
export const pokemonGameResearch = refSchema.table('pokemon_game_research', {
gameId: integer('game_id')
.primaryKey()
.references(() => games.id, { onDelete: 'cascade' }),
@@ -86,7 +87,7 @@ export const pokemonGameResearch = pgTable('pokemon_game_research', {
regions: text('regions').array().notNull()
});
export const pokemonJourneys = pgTable(
export const pokemonJourneys = gamesSchema.table(
'pokemon_journeys',
{
id: id(),
@@ -103,10 +104,13 @@ export const pokemonJourneys = pgTable(
completed: boolean('completed').notNull().default(false),
...timestamps
},
(t) => [unique().on(t.userId, t.gameId)]
(t) => [
unique().on(t.userId, t.gameId),
check('pokemon_journeys_stage_check', sql`${t.stage} IN ('story','postgame','collection')`)
]
);
export const pokemonSeen = pgTable(
export const pokemonSeen = gamesSchema.table(
'pokemon_seen',
{
id: id(),
@@ -124,7 +128,7 @@ export const pokemonSeen = pgTable(
(t) => [unique().on(t.userId, t.gameId, t.pokemonId), index().on(t.userId)]
);
export const pokemonCatches = pgTable(
export const pokemonCatches = gamesSchema.table(
'pokemon_catches',
{
id: id(),
@@ -157,13 +161,14 @@ export const pokemonCatches = pgTable(
(t) => [
index().on(t.userId, t.gameId, t.pokemonId),
unique().on(t.userId, t.gameId, t.partySlot),
check('pokemon_catch_level', sql`${t.level} between 1 and 100`),
check('pokemon_party_slot', sql`${t.partySlot} between 1 and 6`),
check('pokemon_four_moves', sql`cardinality(${t.moves}) <= 4`)
check('pokemon_catches_level_check', sql`${t.level} between 1 and 100`),
check('pokemon_catches_party_slot_check', sql`${t.partySlot} between 1 and 6`),
check('pokemon_catches_moves_check', sql`cardinality(${t.moves}) <= 4`),
check('pokemon_catches_stage_check', sql`${t.stage} IN ('story','postgame','collection')`)
]
);
export const pokemonMilestones = pgTable(
export const pokemonMilestones = gamesSchema.table(
'pokemon_milestones',
{
id: id(),
@@ -184,7 +189,7 @@ export const pokemonMilestones = pgTable(
(t) => [unique().on(t.userId, t.gameId, t.key)]
);
export const trackerRanks = pgTable(
export const trackerRanks = gamesSchema.table(
'tracker_ranks',
{
id: id(),
@@ -200,14 +205,12 @@ export const trackerRanks = pgTable(
},
(t) => [
unique().on(t.userId, t.gameId),
check(
'tracker_rank_range',
sql`${t.level} between 1 and 10000 and ${t.prestige} between 0 and 1000`
)
check('tracker_ranks_level_check', sql`${t.level} between 1 and 10000`),
check('tracker_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`)
]
);
export const trackerWeaponRanks = pgTable(
export const trackerWeaponRanks = gamesSchema.table(
'tracker_weapon_ranks',
{
id: id(),
@@ -223,14 +226,12 @@ export const trackerWeaponRanks = pgTable(
},
(t) => [
unique().on(t.userId, t.itemId),
check(
'tracker_weapon_rank_range',
sql`${t.level} between 1 and 10000 and ${t.prestige} between 0 and 1000`
)
check('tracker_weapon_ranks_level_check', sql`${t.level} between 1 and 10000`),
check('tracker_weapon_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`)
]
);
export const trackerChallenges = pgTable(
export const trackerChallenges = refSchema.table(
'tracker_challenges',
{
id: id(),
@@ -249,7 +250,7 @@ export const trackerChallenges = pgTable(
(t) => [unique().on(t.gameId, t.key)]
);
export const trackerChallengeProgress = pgTable(
export const trackerChallengeProgress = gamesSchema.table(
'tracker_challenge_progress',
{
id: id(),
@@ -263,10 +264,13 @@ export const trackerChallengeProgress = pgTable(
done: boolean('done').notNull().default(false),
...timestamps
},
(t) => [unique().on(t.userId, t.challengeId)]
(t) => [
unique().on(t.userId, t.challengeId),
check('tracker_challenge_progress_current_check', sql`${t.current} >= 0`)
]
);
export const osuEntries = pgTable(
export const osuEntries = gamesSchema.table(
'osu_entries',
{
id: id(),
@@ -282,18 +286,23 @@ export const osuEntries = pgTable(
skill: text('skill').notNull().default('stamina'),
bpm: integer('bpm'),
duration: integer('duration'),
accuracy: text('accuracy'),
accuracy: numeric('accuracy', { mode: 'number' }),
combo: integer('combo'),
misses: integer('misses'),
pp: text('pp'),
pp: numeric('pp', { mode: 'number' }),
mods: text('mods').notNull().default('NM'),
rate: text('rate').notNull().default('1.00'),
rate: numeric('rate', { precision: 4, scale: 2, mode: 'number' })
.notNull()
.default(sql`1.00`),
achieved: boolean('achieved').notNull().default(false),
playedOn: date('played_on').notNull(),
notes: text('notes').notNull().default(''),
...timestamps
},
(t) => [index().on(t.userId, t.kind)]
(t) => [
index().on(t.userId, t.kind),
check('osu_entries_kind_check', sql`${t.kind} IN ('score','goal','training')`)
]
);
export type PokemonCatch = typeof pokemonCatches.$inferSelect;