feat: add database schema and connection pool
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import * as schema from './schema';
|
||||
|
||||
const pool = mysql.createPool({
|
||||
uri: process.env.DATABASE_URL,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, { schema, mode: 'default' });
|
||||
@@ -0,0 +1,423 @@
|
||||
import {
|
||||
mysqlTable, varchar, int, text, boolean, timestamp, json,
|
||||
mysqlEnum, uniqueIndex, index, serial
|
||||
} from 'drizzle-orm/mysql-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// ── Auth ──────────────────────────────────────────
|
||||
|
||||
export const users = mysqlTable('users', {
|
||||
id: serial().primaryKey(),
|
||||
email: varchar({ length: 255 }).notNull().unique(),
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
role: mysqlEnum(['sysadmin', 'director', 'foster_coordinator', 'volunteer_manager', 'vet_liaison', 'content_editor', 'applications_manager', 'viewer', 'foster']).notNull().default('viewer'),
|
||||
active: boolean().notNull().default(false),
|
||||
mustChangePassword: boolean('must_change_password').notNull().default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
|
||||
});
|
||||
|
||||
export const sessions = mysqlTable('sessions', {
|
||||
sessionId: varchar('session_id', { length: 255 }).primaryKey(),
|
||||
userId: int('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
expiresAt: timestamp('expires_at').notNull()
|
||||
});
|
||||
|
||||
// ── Pets ──────────────────────────────────────────
|
||||
|
||||
export const intakeSources = mysqlTable('intake_sources', {
|
||||
id: serial().primaryKey(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
active: boolean().notNull().default(true)
|
||||
});
|
||||
|
||||
export const vets = mysqlTable('vets', {
|
||||
id: serial().primaryKey(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
clinic: varchar({ length: 255 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
email: varchar({ length: 255 }),
|
||||
address: text(),
|
||||
notes: text(),
|
||||
active: boolean().notNull().default(true),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const pets = mysqlTable('pets', {
|
||||
id: serial().primaryKey(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
slug: varchar({ length: 255 }).notNull().unique(),
|
||||
species: mysqlEnum(['dog', 'cat', 'other']).notNull().default('dog'),
|
||||
breed: varchar({ length: 255 }),
|
||||
color: varchar({ length: 255 }),
|
||||
markings: text(),
|
||||
sex: mysqlEnum(['male', 'female', 'unknown']).notNull().default('unknown'),
|
||||
weight: varchar({ length: 50 }),
|
||||
age: varchar({ length: 100 }),
|
||||
birthday: timestamp(),
|
||||
status: mysqlEnum([
|
||||
'available', 'adopted', 'foster', 'medical_hold', 'behavioral_hold',
|
||||
'pending', 'trial', 'hospice', 'nursing', 'whelping', 'deceased', 'transferred'
|
||||
]).notNull().default('available'),
|
||||
description: text(),
|
||||
medicalNotes: text('medical_notes'),
|
||||
behavioralNotes: text('behavioral_notes'),
|
||||
specialNeeds: text('special_needs'),
|
||||
goodWithDogs: boolean('good_with_dogs'),
|
||||
goodWithCats: boolean('good_with_cats'),
|
||||
goodWithKids: boolean('good_with_kids'),
|
||||
houseTrained: boolean('house_trained'),
|
||||
spayedNeutered: boolean('spayed_neutered'),
|
||||
microchipped: boolean(),
|
||||
microchipId: varchar('microchip_id', { length: 100 }),
|
||||
adoptionFee: int('adoption_fee'),
|
||||
adoptionFeeCollected: boolean('adoption_fee_collected').default(false),
|
||||
intakeDate: timestamp('intake_date'),
|
||||
intakeSourceId: int('intake_source_id').references(() => intakeSources.id),
|
||||
adoptedDate: timestamp('adopted_date'),
|
||||
adopterName: varchar('adopter_name', { length: 255 }),
|
||||
adopterEmail: varchar('adopter_email', { length: 255 }),
|
||||
adopterPhone: varchar('adopter_phone', { length: 50 }),
|
||||
vetId: int('vet_id').references(() => vets.id),
|
||||
featured: boolean().notNull().default(false),
|
||||
published: boolean().notNull().default(true),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
|
||||
}, (table) => [
|
||||
index('status_idx').on(table.status),
|
||||
index('species_idx').on(table.species),
|
||||
index('featured_idx').on(table.featured)
|
||||
]);
|
||||
|
||||
export const petPhotos = mysqlTable('pet_photos', {
|
||||
id: serial().primaryKey(),
|
||||
petId: int('pet_id').notNull().references(() => pets.id, { onDelete: 'cascade' }),
|
||||
url: varchar({ length: 500 }).notNull(),
|
||||
alt: varchar({ length: 255 }),
|
||||
isPrimary: boolean('is_primary').notNull().default(false),
|
||||
sortOrder: int('sort_order').notNull().default(0),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const petMedical = mysqlTable('pet_medical', {
|
||||
id: serial().primaryKey(),
|
||||
petId: int('pet_id').notNull().references(() => pets.id, { onDelete: 'cascade' }),
|
||||
type: mysqlEnum([
|
||||
'vaccine', 'surgery', 'checkup', 'medication', 'deworming',
|
||||
'flea_tick', 'heartworm_test', 'heartworm_treatment',
|
||||
'dental', 'spay_neuter', 'microchip', 'other'
|
||||
]).notNull(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
date: timestamp(),
|
||||
vetId: int('vet_id').references(() => vets.id),
|
||||
cost: int(),
|
||||
notes: text(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const fosterAssignments = mysqlTable('foster_assignments', {
|
||||
id: serial().primaryKey(),
|
||||
petId: int('pet_id').notNull().references(() => pets.id, { onDelete: 'cascade' }),
|
||||
userId: int('user_id').notNull().references(() => users.id),
|
||||
startDate: timestamp('start_date').notNull().defaultNow(),
|
||||
endDate: timestamp('end_date'),
|
||||
isCurrent: boolean('is_current').notNull().default(true),
|
||||
notes: text(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const mentors = mysqlTable('mentors', {
|
||||
id: serial().primaryKey(),
|
||||
userId: int('user_id').notNull().references(() => users.id).unique(),
|
||||
bio: text(),
|
||||
active: boolean().notNull().default(true),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const petMentors = mysqlTable('pet_mentors', {
|
||||
id: serial().primaryKey(),
|
||||
petId: int('pet_id').notNull().references(() => pets.id, { onDelete: 'cascade' }),
|
||||
mentorId: int('mentor_id').notNull().references(() => mentors.id, { onDelete: 'cascade' }),
|
||||
assignedAt: timestamp('assigned_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
// ── Applications ──────────────────────────────────
|
||||
|
||||
export const applications = mysqlTable('applications', {
|
||||
id: serial().primaryKey(),
|
||||
type: mysqlEnum(['adoption', 'foster', 'volunteer', 'surrender']).notNull(),
|
||||
status: mysqlEnum(['pending', 'reviewing', 'approved', 'denied', 'withdrawn']).notNull().default('pending'),
|
||||
applicantName: varchar('applicant_name', { length: 255 }).notNull(),
|
||||
applicantEmail: varchar('applicant_email', { length: 255 }).notNull(),
|
||||
applicantPhone: varchar('applicant_phone', { length: 50 }),
|
||||
petId: int('pet_id').references(() => pets.id),
|
||||
formData: json('form_data').notNull(),
|
||||
reviewedBy: int('reviewed_by').references(() => users.id),
|
||||
reviewedAt: timestamp('reviewed_at'),
|
||||
reviewNotes: text('review_notes'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
|
||||
});
|
||||
|
||||
export const applicationHomevisitNotes = mysqlTable('application_homevisit_notes', {
|
||||
id: serial().primaryKey(),
|
||||
applicationId: int('application_id').notNull().references(() => applications.id, { onDelete: 'cascade' }),
|
||||
userId: int('user_id').notNull().references(() => users.id),
|
||||
notes: text().notNull(),
|
||||
visitDate: timestamp('visit_date'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
// ── Finance ───────────────────────────────────────
|
||||
|
||||
export const sponsors = mysqlTable('sponsors', {
|
||||
id: serial().primaryKey(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
tier: mysqlEnum(['top_dog', 'wagging_tails', 'wet_noses']).notNull().default('wet_noses'),
|
||||
logoUrl: varchar('logo_url', { length: 500 }),
|
||||
websiteUrl: varchar('website_url', { length: 500 }),
|
||||
description: text(),
|
||||
active: boolean().notNull().default(true),
|
||||
sortOrder: int('sort_order').notNull().default(0),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const donations = mysqlTable('donations', {
|
||||
id: serial().primaryKey(),
|
||||
donorName: varchar('donor_name', { length: 255 }),
|
||||
donorEmail: varchar('donor_email', { length: 255 }),
|
||||
amount: int().notNull(),
|
||||
source: mysqlEnum([
|
||||
'stripe', 'paypal', 'venmo', 'givebutter', 'cash',
|
||||
'check', 'zelle', 'google_pay', 'square', 'other'
|
||||
]).notNull().default('other'),
|
||||
petId: int('pet_id').references(() => pets.id),
|
||||
stripePaymentId: varchar('stripe_payment_id', { length: 255 }),
|
||||
isRecurring: boolean('is_recurring').notNull().default(false),
|
||||
purpose: varchar({ length: 255 }),
|
||||
notes: text(),
|
||||
date: timestamp().notNull().defaultNow(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const expenses = mysqlTable('expenses', {
|
||||
id: serial().primaryKey(),
|
||||
description: varchar({ length: 255 }).notNull(),
|
||||
amount: int().notNull(),
|
||||
category: varchar({ length: 100 }),
|
||||
petId: int('pet_id').references(() => pets.id),
|
||||
vetId: int('vet_id').references(() => vets.id),
|
||||
receiptUrl: varchar('receipt_url', { length: 500 }),
|
||||
date: timestamp().notNull().defaultNow(),
|
||||
notes: text(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
// ── Content ───────────────────────────────────────
|
||||
|
||||
export const events = mysqlTable('events', {
|
||||
id: serial().primaryKey(),
|
||||
title: varchar({ length: 255 }).notNull(),
|
||||
description: text(),
|
||||
location: varchar({ length: 255 }),
|
||||
startDate: timestamp('start_date').notNull(),
|
||||
endDate: timestamp('end_date'),
|
||||
imageUrl: varchar('image_url', { length: 500 }),
|
||||
published: boolean().notNull().default(true),
|
||||
facebookEventId: varchar('facebook_event_id', { length: 100 }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const happyTails = mysqlTable('happy_tails', {
|
||||
id: serial().primaryKey(),
|
||||
petId: int('pet_id').references(() => pets.id),
|
||||
title: varchar({ length: 255 }).notNull(),
|
||||
story: text().notNull(),
|
||||
imageUrl: varchar('image_url', { length: 500 }),
|
||||
adopterName: varchar('adopter_name', { length: 255 }),
|
||||
featured: boolean().notNull().default(false),
|
||||
published: boolean().notNull().default(false),
|
||||
submittedBy: varchar('submitted_by', { length: 255 }),
|
||||
submittedEmail: varchar('submitted_email', { length: 255 }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const contentBlocks = mysqlTable('content_blocks', {
|
||||
id: serial().primaryKey(),
|
||||
page: varchar({ length: 100 }).notNull(),
|
||||
sectionKey: varchar('section_key', { length: 100 }).notNull(),
|
||||
content: json().notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
|
||||
}, (table) => [
|
||||
uniqueIndex('page_section_idx').on(table.page, table.sectionKey)
|
||||
]);
|
||||
|
||||
export const newsletterSubscribers = mysqlTable('newsletter_subscribers', {
|
||||
id: serial().primaryKey(),
|
||||
email: varchar({ length: 255 }).notNull().unique(),
|
||||
name: varchar({ length: 255 }),
|
||||
subscribedAt: timestamp('subscribed_at').notNull().defaultNow(),
|
||||
unsubscribedAt: timestamp('unsubscribed_at')
|
||||
});
|
||||
|
||||
export const contactMessages = mysqlTable('contact_messages', {
|
||||
id: serial().primaryKey(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
email: varchar({ length: 255 }).notNull(),
|
||||
subject: varchar({ length: 255 }),
|
||||
message: text().notNull(),
|
||||
read: boolean().notNull().default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
// ── Foster Portal ─────────────────────────────────
|
||||
|
||||
export const fosterSubmissions = mysqlTable('foster_submissions', {
|
||||
id: serial().primaryKey(),
|
||||
userId: int('user_id').notNull().references(() => users.id),
|
||||
petId: int('pet_id').notNull().references(() => pets.id, { onDelete: 'cascade' }),
|
||||
type: mysqlEnum(['note', 'photo', 'update']).notNull().default('update'),
|
||||
content: text(),
|
||||
photoUrl: varchar('photo_url', { length: 500 }),
|
||||
status: mysqlEnum(['pending', 'approved', 'rejected']).notNull().default('pending'),
|
||||
reviewedBy: int('reviewed_by').references(() => users.id),
|
||||
reviewedAt: timestamp('reviewed_at'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
export const supplyRequests = mysqlTable('supply_requests', {
|
||||
id: serial().primaryKey(),
|
||||
userId: int('user_id').notNull().references(() => users.id),
|
||||
petId: int('pet_id').references(() => pets.id),
|
||||
items: text().notNull(),
|
||||
urgency: mysqlEnum(['low', 'medium', 'high']).notNull().default('medium'),
|
||||
status: mysqlEnum(['pending', 'approved', 'fulfilled', 'denied']).notNull().default('pending'),
|
||||
notes: text(),
|
||||
reviewedBy: int('reviewed_by').references(() => users.id),
|
||||
reviewedAt: timestamp('reviewed_at'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
// ── Config & System ───────────────────────────────
|
||||
|
||||
export const siteSettings = mysqlTable('site_settings', {
|
||||
id: serial().primaryKey(),
|
||||
key: varchar({ length: 100 }).notNull().unique(),
|
||||
value: text().notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
|
||||
});
|
||||
|
||||
export const orgConfig = mysqlTable('org_config', {
|
||||
id: serial().primaryKey(),
|
||||
key: varchar({ length: 100 }).notNull().unique(),
|
||||
value: json().notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
|
||||
});
|
||||
|
||||
export const auditLog = mysqlTable('audit_log', {
|
||||
id: serial().primaryKey(),
|
||||
userId: int('user_id').references(() => users.id),
|
||||
action: varchar({ length: 100 }).notNull(),
|
||||
entity: varchar({ length: 100 }),
|
||||
entityId: int('entity_id'),
|
||||
diff: json(),
|
||||
ipAddress: varchar('ip_address', { length: 45 }),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
}, (table) => [
|
||||
index('action_idx').on(table.action),
|
||||
index('entity_idx').on(table.entity, table.entityId)
|
||||
]);
|
||||
|
||||
export const pageViews = mysqlTable('page_views', {
|
||||
id: serial().primaryKey(),
|
||||
path: varchar({ length: 500 }).notNull(),
|
||||
petId: int('pet_id').references(() => pets.id),
|
||||
ipAddress: varchar('ip_address', { length: 45 }),
|
||||
userAgent: text('user_agent'),
|
||||
referrer: varchar({ length: 500 }),
|
||||
isBot: boolean('is_bot').notNull().default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
}, (table) => [
|
||||
index('path_idx').on(table.path),
|
||||
index('created_idx').on(table.createdAt)
|
||||
]);
|
||||
|
||||
export const aiJobs = mysqlTable('ai_jobs', {
|
||||
id: serial().primaryKey(),
|
||||
type: varchar({ length: 50 }).notNull(),
|
||||
input: json().notNull(),
|
||||
status: mysqlEnum(['pending', 'processing', 'completed', 'failed']).notNull().default('pending'),
|
||||
output: json(),
|
||||
error: text(),
|
||||
completedAt: timestamp('completed_at'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow()
|
||||
});
|
||||
|
||||
// ── Relations ─────────────────────────────────────
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
sessions: many(sessions),
|
||||
fosterAssignments: many(fosterAssignments),
|
||||
fosterSubmissions: many(fosterSubmissions),
|
||||
supplyRequests: many(supplyRequests)
|
||||
}));
|
||||
|
||||
export const sessionsRelations = relations(sessions, ({ one }) => ({
|
||||
user: one(users, { fields: [sessions.userId], references: [users.id] })
|
||||
}));
|
||||
|
||||
export const petsRelations = relations(pets, ({ one, many }) => ({
|
||||
photos: many(petPhotos),
|
||||
medical: many(petMedical),
|
||||
fosterAssignments: many(fosterAssignments),
|
||||
applications: many(applications),
|
||||
intakeSource: one(intakeSources, { fields: [pets.intakeSourceId], references: [intakeSources.id] }),
|
||||
vet: one(vets, { fields: [pets.vetId], references: [vets.id] }),
|
||||
donations: many(donations),
|
||||
expenses: many(expenses)
|
||||
}));
|
||||
|
||||
export const petPhotosRelations = relations(petPhotos, ({ one }) => ({
|
||||
pet: one(pets, { fields: [petPhotos.petId], references: [pets.id] })
|
||||
}));
|
||||
|
||||
export const petMedicalRelations = relations(petMedical, ({ one }) => ({
|
||||
pet: one(pets, { fields: [petMedical.petId], references: [pets.id] }),
|
||||
vet: one(vets, { fields: [petMedical.vetId], references: [vets.id] })
|
||||
}));
|
||||
|
||||
export const fosterAssignmentsRelations = relations(fosterAssignments, ({ one }) => ({
|
||||
pet: one(pets, { fields: [fosterAssignments.petId], references: [pets.id] }),
|
||||
user: one(users, { fields: [fosterAssignments.userId], references: [users.id] })
|
||||
}));
|
||||
|
||||
export const applicationsRelations = relations(applications, ({ one, many }) => ({
|
||||
pet: one(pets, { fields: [applications.petId], references: [pets.id] }),
|
||||
reviewer: one(users, { fields: [applications.reviewedBy], references: [users.id] }),
|
||||
homevisitNotes: many(applicationHomevisitNotes)
|
||||
}));
|
||||
|
||||
export const applicationHomevisitNotesRelations = relations(applicationHomevisitNotes, ({ one }) => ({
|
||||
application: one(applications, { fields: [applicationHomevisitNotes.applicationId], references: [applications.id] }),
|
||||
user: one(users, { fields: [applicationHomevisitNotes.userId], references: [users.id] })
|
||||
}));
|
||||
|
||||
export const donationsRelations = relations(donations, ({ one }) => ({
|
||||
pet: one(pets, { fields: [donations.petId], references: [pets.id] })
|
||||
}));
|
||||
|
||||
export const expensesRelations = relations(expenses, ({ one }) => ({
|
||||
pet: one(pets, { fields: [expenses.petId], references: [pets.id] }),
|
||||
vet: one(vets, { fields: [expenses.vetId], references: [vets.id] })
|
||||
}));
|
||||
|
||||
export const fosterSubmissionsRelations = relations(fosterSubmissions, ({ one }) => ({
|
||||
user: one(users, { fields: [fosterSubmissions.userId], references: [users.id] }),
|
||||
pet: one(pets, { fields: [fosterSubmissions.petId], references: [pets.id] })
|
||||
}));
|
||||
|
||||
export const supplyRequestsRelations = relations(supplyRequests, ({ one }) => ({
|
||||
user: one(users, { fields: [supplyRequests.userId], references: [users.id] }),
|
||||
pet: one(pets, { fields: [supplyRequests.petId], references: [pets.id] })
|
||||
}));
|
||||
Reference in New Issue
Block a user