Adds upload_links.contact_email and the uploadLink mail template. The column is nullable on purpose. Links already exist in QA and a migration cannot invent addresses for them, so they are grandfathered rather than backfilled with something untrue; the requirement belongs in the create route, which is where new links are actually made. The template requires submitUrl, the same guard verification has on verifyUrl. An email inviting somebody to send in photos, with no way for them to do it, sends perfectly happily and looks fine in the log — it is the one failure here worth making impossible, and a test asserts the default body satisfies the guard it declares. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
342 lines
13 KiB
TypeScript
342 lines
13 KiB
TypeScript
import { pgTable, index, foreignKey, serial, text, integer, timestamp, unique, boolean, jsonb, uniqueIndex, primaryKey, pgSequence } from "drizzle-orm/pg-core"
|
|
import { sql } from "drizzle-orm"
|
|
|
|
|
|
export const pgmigrationsIdSeq = pgSequence("pgmigrations_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
|
|
|
export const items = pgTable("items", {
|
|
id: serial().primaryKey().notNull(),
|
|
name: text().notNull(),
|
|
description: text(),
|
|
priceCents: integer("price_cents").default(8000).notNull(),
|
|
status: text().default('pending').notNull(),
|
|
reservedUntil: timestamp("reserved_until", { withTimezone: true, mode: 'string' }),
|
|
soldAt: timestamp("sold_at", { withTimezone: true, mode: 'string' }),
|
|
paypalOrderId: text("paypal_order_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
categoryId: integer("category_id"),
|
|
}, (table) => [
|
|
index("items_category_id_idx").using("btree", table.categoryId.asc().nullsLast().op("int4_ops")),
|
|
foreignKey({
|
|
columns: [table.categoryId],
|
|
foreignColumns: [categories.id],
|
|
name: "items_category_id_fkey"
|
|
}).onDelete("set null"),
|
|
]);
|
|
|
|
export const itemImages = pgTable("item_images", {
|
|
id: serial().primaryKey().notNull(),
|
|
itemId: integer("item_id").notNull(),
|
|
imagePath: text("image_path").notNull(),
|
|
sortOrder: integer("sort_order").default(0).notNull(),
|
|
originalImagePath: text("original_image_path"),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "item_images_item_id_fkey"
|
|
}).onDelete("cascade"),
|
|
]);
|
|
|
|
export const customerSessions = pgTable("customer_sessions", {
|
|
token: text().primaryKey().notNull(),
|
|
customerId: integer("customer_id").notNull(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "customer_sessions_customer_id_fkey"
|
|
}).onDelete("cascade"),
|
|
]);
|
|
|
|
export const customerTokens = pgTable("customer_tokens", {
|
|
token: text().primaryKey().notNull(),
|
|
customerId: integer("customer_id").notNull(),
|
|
kind: text().notNull(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "customer_tokens_customer_id_fkey"
|
|
}).onDelete("cascade"),
|
|
]);
|
|
|
|
export const adminSettings = pgTable("admin_settings", {
|
|
key: text().primaryKey().notNull(),
|
|
value: text().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
});
|
|
|
|
export const customers = pgTable("customers", {
|
|
id: serial().primaryKey().notNull(),
|
|
email: text().notNull(),
|
|
passwordHash: text("password_hash").notNull(),
|
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
marketingConsent: boolean("marketing_consent").default(false).notNull(),
|
|
marketingConsentAt: timestamp("marketing_consent_at", { withTimezone: true, mode: 'string' }),
|
|
marketingConsentText: text("marketing_consent_text"),
|
|
unsubscribeToken: text("unsubscribe_token").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
disabledAt: timestamp("disabled_at", { withTimezone: true, mode: 'string' }),
|
|
favoriteAlerts: boolean("favorite_alerts").default(false).notNull(),
|
|
favoriteAlertsAt: timestamp("favorite_alerts_at", { withTimezone: true, mode: 'string' }),
|
|
favoriteAlertsText: text("favorite_alerts_text"),
|
|
firstName: text("first_name"),
|
|
lastName: text("last_name"),
|
|
}, (table) => [
|
|
index("customers_disabled_at_idx").using("btree", table.disabledAt.asc().nullsLast().op("timestamptz_ops")).where(sql`(disabled_at IS NOT NULL)`),
|
|
unique("customers_email_key").on(table.email),
|
|
unique("customers_unsubscribe_token_key").on(table.unsubscribeToken),
|
|
]);
|
|
|
|
export const carts = pgTable("carts", {
|
|
id: serial().primaryKey().notNull(),
|
|
customerId: integer("customer_id").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "carts_customer_id_fkey"
|
|
}).onDelete("cascade"),
|
|
unique("carts_customer_id_key").on(table.customerId),
|
|
]);
|
|
|
|
export const cartItems = pgTable("cart_items", {
|
|
id: serial().primaryKey().notNull(),
|
|
cartId: integer("cart_id").notNull(),
|
|
itemId: integer("item_id").notNull(),
|
|
addedAt: timestamp("added_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(),
|
|
lastReminderSentAt: timestamp("last_reminder_sent_at", { withTimezone: true, mode: 'string' }),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.cartId],
|
|
foreignColumns: [carts.id],
|
|
name: "cart_items_cart_id_fkey"
|
|
}).onDelete("cascade"),
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "cart_items_item_id_fkey"
|
|
}).onDelete("cascade"),
|
|
unique("cart_items_item_id_key").on(table.itemId),
|
|
]);
|
|
|
|
export const shippingAddresses = pgTable("shipping_addresses", {
|
|
id: serial().primaryKey().notNull(),
|
|
customerId: integer("customer_id").notNull(),
|
|
fullName: text("full_name").notNull(),
|
|
addressLine1: text("address_line1").notNull(),
|
|
addressLine2: text("address_line2"),
|
|
city: text().notNull(),
|
|
state: text().notNull(),
|
|
postalCode: text("postal_code").notNull(),
|
|
country: text().default('US').notNull(),
|
|
isDefault: boolean("is_default").default(false).notNull(),
|
|
uspsValidated: boolean("usps_validated").default(false).notNull(),
|
|
uspsStandardized: jsonb("usps_standardized"),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "shipping_addresses_customer_id_fkey"
|
|
}).onDelete("cascade"),
|
|
]);
|
|
|
|
export const checkouts = pgTable("checkouts", {
|
|
id: serial().primaryKey().notNull(),
|
|
customerId: integer("customer_id"),
|
|
shippingAddressId: integer("shipping_address_id"),
|
|
processor: text().notNull(),
|
|
processorOrderId: text("processor_order_id"),
|
|
amountCents: integer("amount_cents"),
|
|
status: text().default('pending').notNull(),
|
|
rawEvent: jsonb("raw_event"),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "checkouts_customer_id_fkey"
|
|
}).onDelete("set null"),
|
|
foreignKey({
|
|
columns: [table.shippingAddressId],
|
|
foreignColumns: [shippingAddresses.id],
|
|
name: "checkouts_shipping_address_id_fkey"
|
|
}).onDelete("set null"),
|
|
]);
|
|
|
|
export const orders = pgTable("orders", {
|
|
id: serial().primaryKey().notNull(),
|
|
itemId: integer("item_id"),
|
|
customerId: integer("customer_id"),
|
|
checkoutId: integer("checkout_id"),
|
|
processor: text().notNull(),
|
|
processorOrderId: text("processor_order_id"),
|
|
amountCents: integer("amount_cents"),
|
|
status: text(),
|
|
rawEvent: jsonb("raw_event"),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "orders_item_id_fkey"
|
|
}),
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "orders_customer_id_fkey"
|
|
}).onDelete("set null"),
|
|
foreignKey({
|
|
columns: [table.checkoutId],
|
|
foreignColumns: [checkouts.id],
|
|
name: "orders_checkout_id_fkey"
|
|
}).onDelete("set null"),
|
|
]);
|
|
|
|
export const categories = pgTable("categories", {
|
|
id: serial().primaryKey().notNull(),
|
|
name: text().notNull(),
|
|
parentId: integer("parent_id"),
|
|
sortOrder: integer("sort_order").default(0).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
uniqueIndex("categories_child_name_uniq").using("btree", sql`parent_id`, sql`lower(name)`).where(sql`(parent_id IS NOT NULL)`),
|
|
index("categories_parent_id_idx").using("btree", table.parentId.asc().nullsLast().op("int4_ops")),
|
|
uniqueIndex("categories_root_name_uniq").using("btree", sql`lower(name)`).where(sql`(parent_id IS NULL)`),
|
|
foreignKey({
|
|
columns: [table.parentId],
|
|
foreignColumns: [table.id],
|
|
name: "categories_parent_id_fkey"
|
|
}).onDelete("cascade"),
|
|
]);
|
|
|
|
export const tags = pgTable("tags", {
|
|
id: serial().primaryKey().notNull(),
|
|
name: text().notNull(),
|
|
color: text().notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`),
|
|
]);
|
|
|
|
export const itemDrafts = pgTable("item_drafts", {
|
|
id: serial().primaryKey().notNull(),
|
|
itemId: integer("item_id").notNull(),
|
|
uploadLinkId: integer("upload_link_id"),
|
|
submitterNote: text("submitter_note"),
|
|
removeBackground: boolean("remove_background").default(true).notNull(),
|
|
state: text().default('queued').notNull(),
|
|
attempts: integer().default(0).notNull(),
|
|
model: text(),
|
|
aiName: text("ai_name"),
|
|
aiDescription: text("ai_description"),
|
|
aiCategoryId: integer("ai_category_id"),
|
|
aiTagNames: text("ai_tag_names").array(),
|
|
aiSuggestedPriceCents: integer("ai_suggested_price_cents"),
|
|
priceSource: text("price_source").default('default').notNull(),
|
|
aiError: text("ai_error"),
|
|
inputTokens: integer("input_tokens"),
|
|
outputTokens: integer("output_tokens"),
|
|
costMicros: integer("cost_micros"),
|
|
draftedAt: timestamp("drafted_at", { withTimezone: true, mode: 'string' }),
|
|
reviewedAt: timestamp("reviewed_at", { withTimezone: true, mode: 'string' }),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
index("item_drafts_state_idx").using("btree", table.state.asc().nullsLast().op("text_ops")),
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "item_drafts_item_id_fkey"
|
|
}).onDelete("cascade"),
|
|
foreignKey({
|
|
columns: [table.uploadLinkId],
|
|
foreignColumns: [uploadLinks.id],
|
|
name: "item_drafts_upload_link_id_fkey"
|
|
}).onDelete("set null"),
|
|
foreignKey({
|
|
columns: [table.aiCategoryId],
|
|
foreignColumns: [categories.id],
|
|
name: "item_drafts_ai_category_id_fkey"
|
|
}).onDelete("set null"),
|
|
unique("item_drafts_item_id_key").on(table.itemId),
|
|
]);
|
|
|
|
export const uploadLinks = pgTable("upload_links", {
|
|
id: serial().primaryKey().notNull(),
|
|
label: text().notNull(),
|
|
tokenHash: text("token_hash").notNull(),
|
|
revokedAt: timestamp("revoked_at", { withTimezone: true, mode: 'string' }),
|
|
submissionCount: integer("submission_count").default(0).notNull(),
|
|
maxSubmissions: integer("max_submissions"),
|
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true, mode: 'string' }),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
contactEmail: text("contact_email"),
|
|
}, (table) => [
|
|
unique("upload_links_token_hash_key").on(table.tokenHash),
|
|
]);
|
|
|
|
export const itemTags = pgTable("item_tags", {
|
|
itemId: integer("item_id").notNull(),
|
|
tagId: integer("tag_id").notNull(),
|
|
}, (table) => [
|
|
index("item_tags_tag_id_idx").using("btree", table.tagId.asc().nullsLast().op("int4_ops")),
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "item_tags_item_id_fkey"
|
|
}).onDelete("cascade"),
|
|
foreignKey({
|
|
columns: [table.tagId],
|
|
foreignColumns: [tags.id],
|
|
name: "item_tags_tag_id_fkey"
|
|
}).onDelete("cascade"),
|
|
primaryKey({ columns: [table.itemId, table.tagId], name: "item_tags_pkey"}),
|
|
]);
|
|
|
|
export const checkoutItems = pgTable("checkout_items", {
|
|
checkoutId: integer("checkout_id").notNull(),
|
|
itemId: integer("item_id").notNull(),
|
|
priceCents: integer("price_cents").notNull(),
|
|
}, (table) => [
|
|
foreignKey({
|
|
columns: [table.checkoutId],
|
|
foreignColumns: [checkouts.id],
|
|
name: "checkout_items_checkout_id_fkey"
|
|
}).onDelete("cascade"),
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "checkout_items_item_id_fkey"
|
|
}).onDelete("cascade"),
|
|
primaryKey({ columns: [table.checkoutId, table.itemId], name: "checkout_items_pkey"}),
|
|
]);
|
|
|
|
export const favorites = pgTable("favorites", {
|
|
customerId: integer("customer_id").notNull(),
|
|
itemId: integer("item_id").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
|
}, (table) => [
|
|
index("favorites_item_id_idx").using("btree", table.itemId.asc().nullsLast().op("int4_ops")),
|
|
foreignKey({
|
|
columns: [table.customerId],
|
|
foreignColumns: [customers.id],
|
|
name: "favorites_customer_id_fkey"
|
|
}).onDelete("cascade"),
|
|
foreignKey({
|
|
columns: [table.itemId],
|
|
foreignColumns: [items.id],
|
|
name: "favorites_item_id_fkey"
|
|
}).onDelete("cascade"),
|
|
primaryKey({ columns: [table.customerId, table.itemId], name: "favorites_pkey"}),
|
|
]);
|