Add missing DB indexes, NOT NULL on clients.email, and S3 error handling
- Add 4 indexes on appointments: client_id, staff_id, start_time, status - Add index on pets.client_id - Add index on clients.email - Change clients.email to NOT NULL with backfill migration - Wrap S3 deleteObject calls in try/catch in pets photo endpoints - Update POST /clients test to include required email field Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
-- Migration: 0029_db_indexes_constraints.sql
|
||||||
|
-- Add missing indexes on appointments, pets, clients tables and NOT NULL constraint on clients.email
|
||||||
|
|
||||||
|
-- Backfill NULL emails before setting NOT NULL
|
||||||
|
UPDATE clients SET email = concat('unknown-', id::text, '@placeholder.local') WHERE email IS NULL;
|
||||||
|
|
||||||
|
-- Add indexes on appointments table
|
||||||
|
CREATE INDEX idx_appointments_client_id ON appointments(client_id);
|
||||||
|
CREATE INDEX idx_appointments_staff_id ON appointments(staff_id);
|
||||||
|
CREATE INDEX idx_appointments_start_time ON appointments(start_time);
|
||||||
|
CREATE INDEX idx_appointments_status ON appointments(status);
|
||||||
|
|
||||||
|
-- Add index on pets table
|
||||||
|
CREATE INDEX idx_pets_client_id ON pets(client_id);
|
||||||
|
|
||||||
|
-- Add index on clients table
|
||||||
|
CREATE INDEX idx_clients_email ON clients(email);
|
||||||
|
|
||||||
|
-- Set NOT NULL on clients.email (after backfill)
|
||||||
|
ALTER TABLE clients ALTER COLUMN email SET NOT NULL;
|
||||||
+48
-40
@@ -102,47 +102,55 @@ export const verification = pgTable("verification", {
|
|||||||
|
|
||||||
// ─── Tables ───────────────────────────────────────────────────────────────────
|
// ─── Tables ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
export const clients = pgTable(
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
"clients",
|
||||||
name: text("name").notNull(),
|
{
|
||||||
email: text("email"),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
phone: text("phone"),
|
name: text("name").notNull(),
|
||||||
address: text("address"),
|
email: text("email").notNull(),
|
||||||
notes: text("notes"),
|
phone: text("phone"),
|
||||||
emailOptOut: boolean("email_opt_out").notNull().default(false),
|
address: text("address"),
|
||||||
smsOptIn: boolean("sms_opt_in").notNull().default(false),
|
notes: text("notes"),
|
||||||
smsConsentDate: timestamp("sms_consent_date"),
|
emailOptOut: boolean("email_opt_out").notNull().default(false),
|
||||||
smsOptOutDate: timestamp("sms_opt_out_date"),
|
smsOptIn: boolean("sms_opt_in").notNull().default(false),
|
||||||
smsConsentText: text("sms_consent_text"),
|
smsConsentDate: timestamp("sms_consent_date"),
|
||||||
stripeCustomerId: text("stripe_customer_id"),
|
smsOptOutDate: timestamp("sms_opt_out_date"),
|
||||||
status: clientStatusEnum("status").notNull().default("active"),
|
smsConsentText: text("sms_consent_text"),
|
||||||
disabledAt: timestamp("disabled_at"),
|
stripeCustomerId: text("stripe_customer_id"),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
status: clientStatusEnum("status").notNull().default("active"),
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
disabledAt: timestamp("disabled_at"),
|
||||||
});
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [index("idx_clients_email").on(t.email)]
|
||||||
|
);
|
||||||
|
|
||||||
export const pets = pgTable("pets", {
|
export const pets = pgTable(
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
"pets",
|
||||||
clientId: uuid("client_id")
|
{
|
||||||
.notNull()
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
.references(() => clients.id, { onDelete: "cascade" }),
|
clientId: uuid("client_id")
|
||||||
name: text("name").notNull(),
|
.notNull()
|
||||||
species: text("species").notNull(),
|
.references(() => clients.id, { onDelete: "cascade" }),
|
||||||
breed: text("breed"),
|
name: text("name").notNull(),
|
||||||
weightKg: numeric("weight_kg", { precision: 5, scale: 2 }),
|
species: text("species").notNull(),
|
||||||
dateOfBirth: timestamp("date_of_birth"),
|
breed: text("breed"),
|
||||||
healthAlerts: text("health_alerts"),
|
weightKg: numeric("weight_kg", { precision: 5, scale: 2 }),
|
||||||
groomingNotes: text("grooming_notes"),
|
dateOfBirth: timestamp("date_of_birth"),
|
||||||
cutStyle: text("cut_style"),
|
healthAlerts: text("health_alerts"),
|
||||||
shampooPreference: text("shampoo_preference"),
|
groomingNotes: text("grooming_notes"),
|
||||||
specialCareNotes: text("special_care_notes"),
|
cutStyle: text("cut_style"),
|
||||||
customFields: jsonb("custom_fields").$type<Record<string, string>>().notNull().default({}),
|
shampooPreference: text("shampoo_preference"),
|
||||||
photoKey: text("photo_key"),
|
specialCareNotes: text("special_care_notes"),
|
||||||
photoUploadedAt: timestamp("photo_uploaded_at"),
|
customFields: jsonb("custom_fields").$type<Record<string, string>>().notNull().default({}),
|
||||||
image: text("image"),
|
photoKey: text("photo_key"),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
photoUploadedAt: timestamp("photo_uploaded_at"),
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
image: text("image"),
|
||||||
});
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [index("idx_pets_client_id").on(t.clientId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const services = pgTable("services", {
|
export const services = pgTable("services", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|||||||
Reference in New Issue
Block a user