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:
@@ -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
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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] })
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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')`)]
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')`)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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')
|
||||
|
||||
14
src/lib/server/db/schema/schemas.ts
Normal file
14
src/lib/server/db/schema/schemas.ts
Normal 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');
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user