import { sql } from 'drizzle-orm'; import { sqliteTable, text, integer, real, check } from 'drizzle-orm/sqlite-core'; export const rabbits = sqliteTable( 'rabbits', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), farm: text('farm'), sex: text('sex').notNull().default('unknown'), dob: text('dob'), weightLbs: real('weight_lbs'), earNumber: text('ear_number'), location: text('location'), status: text('status').notNull().default('active'), breed: text('breed'), colorName: text('color_name'), photoPath: text('photo_path'), registrationNumber: text('registration_number'), gcNumber: text('gc_number'), sireId: integer('sire_id').references((): any => rabbits.id), damId: integer('dam_id').references((): any => rabbits.id), // Timeline anchors (see routes/shows.ts and client RabbitTimeline). `dob` is // the timeline's start; `acquiredDate` is only meaningful when the rabbit was // added to the herd after birth (bought-in); `deathDate` records when it died // (independent of the `deceased` status value, which can be set without a date). acquiredDate: text('acquired_date'), deathDate: text('death_date'), // Genetics loci locusA: text('locus_a'), locusB: text('locus_b'), locusC: text('locus_c'), locusD: text('locus_d'), locusE: text('locus_e'), locusEn: text('locus_en'), locusV: text('locus_v'), // Chain-authority freeform ancestor text (see memory-bank/architecture.md) // Generation 1 (parents) sireNameText: text('sire_name_text'), damNameText: text('dam_name_text'), // Generation 2 (grandparents) paternalGrandsireNameText: text('paternal_grandsire_name_text'), paternalGranddamNameText: text('paternal_granddam_name_text'), maternalGrandsireNameText: text('maternal_grandsire_name_text'), maternalGranddamNameText: text('maternal_granddam_name_text'), // Generation 3 (great-grandparents) ppGrandsireNameText: text('pp_grandsire_name_text'), ppGranddamNameText: text('pp_granddam_name_text'), pmGrandsireNameText: text('pm_grandsire_name_text'), pmGranddamNameText: text('pm_granddam_name_text'), mpGrandsireNameText: text('mp_grandsire_name_text'), mpGranddamNameText: text('mp_granddam_name_text'), mmGrandsireNameText: text('mm_grandsire_name_text'), mmGranddamNameText: text('mm_granddam_name_text'), createdAt: text('created_at').notNull().default(sql`(current_timestamp)`), updatedAt: text('updated_at').notNull().default(sql`(current_timestamp)`), }, (table) => [ check('sex_check', sql`${table.sex} in ('buck', 'doe', 'unknown')`), check( 'status_check', sql`${table.status} in ('active', 'for_sale', 'on_hold', 'sold', 'deceased', 'other')` ), ] ); export const litters = sqliteTable('litters', { id: integer('id').primaryKey({ autoIncrement: true }), sireId: integer('sire_id').references(() => rabbits.id), damId: integer('dam_id').references(() => rabbits.id), // A litter record spans the breeding→kindle lifecycle: `breedDate` is set when // the pairing is recorded; `kindleDate` (nullable until the doe kindles) and // `kitCount` are filled in later. At least one of the two dates is required — // enforced in routes/litters.ts, not by the schema. breedDate: text('breed_date'), kindleDate: text('kindle_date'), kitCount: integer('kit_count'), notes: text('notes'), createdAt: text('created_at').notNull().default(sql`(current_timestamp)`), }); // Manually-logged events on a rabbit's timeline. The auto-derived events (birth // from `dob`, breedings/kindles from `litters`) and the column-backed ones // (acquired/death) are NOT stored here — only `show`/`sale`/`other`. `title` is // the timeline headline (show name, sale label, etc.); `showClass`/`placement` // are only meaningful for `type='show'`. A `sale` event also flips the rabbit's // status to `sold` (done in routes/events.ts). See the rabbit-timeline note. export const rabbitEvents = sqliteTable( 'rabbit_events', { id: integer('id').primaryKey({ autoIncrement: true }), rabbitId: integer('rabbit_id') .notNull() .references(() => rabbits.id, { onDelete: 'cascade' }), type: text('type').notNull(), eventDate: text('event_date').notNull(), title: text('title').notNull(), notes: text('notes'), showClass: text('show_class'), placement: text('placement'), createdAt: text('created_at').notNull().default(sql`(current_timestamp)`), }, (table) => [check('event_type_check', sql`${table.type} in ('show', 'sale', 'other')`)] ); export const settings = sqliteTable('settings', { key: text('key').primaryKey(), value: text('value'), }); // Cached snapshot of the ARBA show-search listing (see routes/shows.ts and // lib/arba.ts). ARBA sits behind Cloudflare and only updates Mon–Fri, so we // scrape at most once a day and serve everything else from here. `data` is the // normalized show array stored as a JSON blob; `fetchedAt` drives staleness. // Only ever one row — the latest scrape; an insert is paired with a delete of // older rows in the route. export const arbaShowsCache = sqliteTable('arba_shows_cache', { id: integer('id').primaryKey({ autoIncrement: true }), fetchedAt: text('fetched_at').notNull().default(sql`(current_timestamp)`), data: text('data').notNull(), }); // A user-created grouping of one or more ARBA shows (e.g. a weekend away with // several shows). Single-user app, so there's no user_id (the design doc's // `user_id`/`users` table doesn't exist here). Judges/entries/placements // (Phases 3–4) hang off `show_event_shows`, not this table. export const showEvents = sqliteTable('show_events', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), notes: text('notes'), createdAt: text('created_at').notNull().default(sql`(current_timestamp)`), updatedAt: text('updated_at').notNull().default(sql`(current_timestamp)`), }); // A snapshot of one ARBA show as it was when added to an event. We copy the // fields rather than referencing the cache because the ARBA listing changes // daily and rolls off once a show passes — the event must keep its own record. // `show_type` stores the human-readable full string (e.g. "OPEN ALL BREED // SHOW"); `specialty_breed` is set only for specialty shows. export const showEventShows = sqliteTable('show_event_shows', { id: integer('id').primaryKey({ autoIncrement: true }), showEventId: integer('show_event_id') .notNull() .references(() => showEvents.id, { onDelete: 'cascade' }), sanctionNumber: text('sanction_number').notNull(), showDate: text('show_date'), clubName: text('club_name').notNull(), showType: text('show_type'), specialtyBreed: text('specialty_breed'), city: text('city'), state: text('state'), address: text('address'), secretary: text('secretary'), secretaryEmail: text('secretary_email'), catalogUrl: text('catalog_url'), mapsUrl: text('maps_url'), displayOrder: integer('display_order').default(0), }); // A rabbit from the herd entered into a specific show within an event. // `entry_class` is free text (e.g. "Sr Buck", "6-8 Doe"). Cascades when its // show is removed; the rabbit FK has no cascade (deleting a rabbit shouldn't // silently drop show history — handled in routes/rabbits.ts's delete if needed). // Placements (Phase 4) will reference these rows. export const showEventEntries = sqliteTable('show_event_entries', { id: integer('id').primaryKey({ autoIncrement: true }), showEventShowId: integer('show_event_show_id') .notNull() .references(() => showEventShows.id, { onDelete: 'cascade' }), rabbitId: integer('rabbit_id') .notNull() .references(() => rabbits.id), entryClass: text('entry_class'), entryNumber: text('entry_number'), notes: text('notes'), createdAt: text('created_at').notNull().default(sql`(current_timestamp)`), }); // Judges assigned to a show within an event (Phase 4). Cascades with its show. export const showEventJudges = sqliteTable('show_event_judges', { id: integer('id').primaryKey({ autoIncrement: true }), showEventShowId: integer('show_event_show_id') .notNull() .references(() => showEventShows.id, { onDelete: 'cascade' }), judgeName: text('judge_name').notNull(), judgeArbaNumber: text('judge_arba_number'), notes: text('notes'), }); // Post-show placement for a single entry (Phase 4). One row per entry (the // `entry_id` UNIQUE makes the PUT endpoint a clean upsert). `placement` is free // text (`1st`, `BOB`, `BOSV`, …); `legs_earned` is the leg count (0–3); the four // booleans are quick toggles for the Results Summary rollup. export const showEventPlacements = sqliteTable('show_event_placements', { id: integer('id').primaryKey({ autoIncrement: true }), entryId: integer('entry_id') .notNull() .unique() .references(() => showEventEntries.id, { onDelete: 'cascade' }), placement: text('placement'), legsEarned: integer('legs_earned').notNull().default(0), bob: integer('bob', { mode: 'boolean' }).notNull().default(false), bos: integer('bos', { mode: 'boolean' }).notNull().default(false), bis: integer('bis', { mode: 'boolean' }).notNull().default(false), ris: integer('ris', { mode: 'boolean' }).notNull().default(false), notes: text('notes'), });