Merge main into feature/226-strip-exif
Linting / lint (pull_request) Successful in 2m27s
SonarQube Analysis / sonarqube (pull_request) Failing after 22m53s

main gained the Drizzle spike (#216) after this branch was cut, and both changes add a production dependency, so `backend/package-lock.json` conflicted. `backend/package.json` merged cleanly and carries both `drizzle-orm` and `sharp`.

The lockfile was regenerated rather than hand-merged: main's version taken as the base, then `npm install` re-resolved it. That install was deliberately run under Node 24 rather than the machine's default 18.16.1, because sharp's platform binaries are optional dependencies that npm silently omits when the engine check fails — regenerating this file on Node 18 would have quietly dropped every `@img/sharp-*` entry and produced a lockfile that installs a sharp which cannot load. Verified afterwards that linux-x64, linux-arm64 and win32-x64 are all present and that drizzle-orm survived.

Backend: 285 unit tests pass, tsc clean. Lint reports six warnings rather than three; the three new ones are in src/db-drizzle from the spike, not from this branch.

Ref #226
This commit is contained in:
2026-08-29 14:10:17 -05:00
13 changed files with 5557 additions and 3 deletions
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'drizzle-kit';
// Spike configuration (#216). Credentials come from the environment rather than
// this file — the same rule the rest of the repo follows, and this one points at
// a developer's local database, not a deployed one.
//
// DRIZZLE_DATABASE_URL=postgres://redefined_local:redefined_local@localhost:55500/redefined_local
export default defineConfig({
dialect: 'postgresql',
schema: './src/db-drizzle/schema.ts',
out: './drizzle',
dbCredentials: {
url: process.env.DRIZZLE_DATABASE_URL ?? ''
}
});
@@ -0,0 +1,194 @@
-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE TABLE "pgmigrations" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"run_on" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "admin_settings" (
"key" text PRIMARY KEY NOT NULL,
"value" text NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "item_images" (
"id" serial PRIMARY KEY NOT NULL,
"item_id" integer NOT NULL,
"image_path" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "shipping_addresses" (
"id" serial PRIMARY KEY NOT NULL,
"customer_id" integer NOT NULL,
"full_name" text NOT NULL,
"address_line1" text NOT NULL,
"address_line2" text,
"city" text NOT NULL,
"state" text NOT NULL,
"postal_code" text NOT NULL,
"country" text DEFAULT 'US' NOT NULL,
"is_default" boolean DEFAULT false NOT NULL,
"usps_validated" boolean DEFAULT false NOT NULL,
"usps_standardized" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "items" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"description" text,
"price_cents" integer NOT NULL,
"status" text DEFAULT 'pending' NOT NULL,
"reserved_until" timestamp with time zone,
"sold_at" timestamp with time zone,
"paypal_order_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"category_id" integer
);
--> statement-breakpoint
CREATE TABLE "tags" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"color" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "categories" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"parent_id" integer,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "orders" (
"id" serial PRIMARY KEY NOT NULL,
"item_id" integer,
"customer_id" integer,
"checkout_id" integer,
"processor" text NOT NULL,
"processor_order_id" text,
"amount_cents" integer,
"status" text,
"raw_event" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "cart_items" (
"id" serial PRIMARY KEY NOT NULL,
"cart_id" integer NOT NULL,
"item_id" integer NOT NULL,
"added_at" timestamp with time zone DEFAULT now() NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"last_reminder_sent_at" timestamp with time zone,
CONSTRAINT "cart_items_item_id_key" UNIQUE("item_id")
);
--> statement-breakpoint
CREATE TABLE "carts" (
"id" serial PRIMARY KEY NOT NULL,
"customer_id" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "carts_customer_id_key" UNIQUE("customer_id")
);
--> statement-breakpoint
CREATE TABLE "customer_tokens" (
"token" text PRIMARY KEY NOT NULL,
"customer_id" integer NOT NULL,
"kind" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "checkouts" (
"id" serial PRIMARY KEY NOT NULL,
"customer_id" integer,
"shipping_address_id" integer,
"processor" text NOT NULL,
"processor_order_id" text,
"amount_cents" integer,
"status" text DEFAULT 'pending' NOT NULL,
"raw_event" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "customer_sessions" (
"token" text PRIMARY KEY NOT NULL,
"customer_id" integer NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "customers" (
"id" serial PRIMARY KEY NOT NULL,
"email" text NOT NULL,
"password_hash" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"marketing_consent" boolean DEFAULT false NOT NULL,
"marketing_consent_at" timestamp with time zone,
"marketing_consent_text" text,
"unsubscribe_token" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"disabled_at" timestamp with time zone,
"favorite_alerts" boolean DEFAULT false NOT NULL,
"favorite_alerts_at" timestamp with time zone,
"favorite_alerts_text" text,
"first_name" text,
"last_name" text,
CONSTRAINT "customers_email_key" UNIQUE("email"),
CONSTRAINT "customers_unsubscribe_token_key" UNIQUE("unsubscribe_token")
);
--> statement-breakpoint
CREATE TABLE "item_tags" (
"item_id" integer NOT NULL,
"tag_id" integer NOT NULL,
CONSTRAINT "item_tags_pkey" PRIMARY KEY("item_id","tag_id")
);
--> statement-breakpoint
CREATE TABLE "favorites" (
"customer_id" integer NOT NULL,
"item_id" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "favorites_pkey" PRIMARY KEY("customer_id","item_id")
);
--> statement-breakpoint
CREATE TABLE "checkout_items" (
"checkout_id" integer NOT NULL,
"item_id" integer NOT NULL,
"price_cents" integer NOT NULL,
CONSTRAINT "checkout_items_pkey" PRIMARY KEY("checkout_id","item_id")
);
--> statement-breakpoint
ALTER TABLE "item_images" ADD CONSTRAINT "item_images_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "shipping_addresses" ADD CONSTRAINT "shipping_addresses_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "items" ADD CONSTRAINT "items_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "categories" ADD CONSTRAINT "categories_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "orders" ADD CONSTRAINT "orders_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "orders" ADD CONSTRAINT "orders_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "orders" ADD CONSTRAINT "orders_checkout_id_fkey" FOREIGN KEY ("checkout_id") REFERENCES "public"."checkouts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_cart_id_fkey" FOREIGN KEY ("cart_id") REFERENCES "public"."carts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "carts" ADD CONSTRAINT "carts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "customer_tokens" ADD CONSTRAINT "customer_tokens_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_shipping_address_id_fkey" FOREIGN KEY ("shipping_address_id") REFERENCES "public"."shipping_addresses"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "customer_sessions" ADD CONSTRAINT "customer_sessions_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "item_tags" ADD CONSTRAINT "item_tags_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "item_tags" ADD CONSTRAINT "item_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "favorites" ADD CONSTRAINT "favorites_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "favorites" ADD CONSTRAINT "favorites_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "checkout_items" ADD CONSTRAINT "checkout_items_checkout_id_fkey" FOREIGN KEY ("checkout_id") REFERENCES "public"."checkouts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "checkout_items" ADD CONSTRAINT "checkout_items_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "items_category_id_idx" ON "items" USING btree ("category_id" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "tags_name_uniq" ON "tags" USING btree (lower(name) text_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "categories_child_name_uniq" ON "categories" USING btree (parent_id text_ops,lower(name) int4_ops) WHERE (parent_id IS NOT NULL);--> statement-breakpoint
CREATE INDEX "categories_parent_id_idx" ON "categories" USING btree ("parent_id" int4_ops);--> statement-breakpoint
CREATE UNIQUE INDEX "categories_root_name_uniq" ON "categories" USING btree (lower(name) text_ops) WHERE (parent_id IS NULL);--> statement-breakpoint
CREATE INDEX "customers_disabled_at_idx" ON "customers" USING btree ("disabled_at" timestamptz_ops) WHERE (disabled_at IS NOT NULL);--> statement-breakpoint
CREATE INDEX "item_tags_tag_id_idx" ON "item_tags" USING btree ("tag_id" int4_ops);--> statement-breakpoint
CREATE INDEX "favorites_item_id_idx" ON "favorites" USING btree ("item_id" int4_ops);
*/
@@ -0,0 +1,7 @@
DROP INDEX "tags_name_uniq";--> statement-breakpoint
DROP INDEX "categories_child_name_uniq";--> statement-breakpoint
DROP INDEX "categories_root_name_uniq";--> statement-breakpoint
ALTER TABLE "items" ADD COLUMN "condition_note" text;--> statement-breakpoint
CREATE UNIQUE INDEX "tags_name_uniq" ON "tags" USING btree (lower(name));--> statement-breakpoint
CREATE UNIQUE INDEX "categories_child_name_uniq" ON "categories" USING btree (parent_id,lower(name)) WHERE (parent_id IS NOT NULL);--> statement-breakpoint
CREATE UNIQUE INDEX "categories_root_name_uniq" ON "categories" USING btree (lower(name)) WHERE (parent_id IS NULL);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1787946791717,
"tag": "0000_sleepy_franklin_richards",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1787946975565,
"tag": "0001_add_condition_note",
"breakpoints": true
}
]
}
+150
View File
@@ -0,0 +1,150 @@
import { relations } from "drizzle-orm/relations";
import { items, itemImages, customers, shippingAddresses, categories, orders, checkouts, carts, cartItems, customerTokens, customerSessions, itemTags, tags, favorites, checkoutItems } from "./schema";
export const itemImagesRelations = relations(itemImages, ({one}) => ({
item: one(items, {
fields: [itemImages.itemId],
references: [items.id]
}),
}));
export const itemsRelations = relations(items, ({one, many}) => ({
itemImages: many(itemImages),
category: one(categories, {
fields: [items.categoryId],
references: [categories.id]
}),
orders: many(orders),
cartItems: many(cartItems),
itemTags: many(itemTags),
favorites: many(favorites),
checkoutItems: many(checkoutItems),
}));
export const shippingAddressesRelations = relations(shippingAddresses, ({one, many}) => ({
customer: one(customers, {
fields: [shippingAddresses.customerId],
references: [customers.id]
}),
checkouts: many(checkouts),
}));
export const customersRelations = relations(customers, ({many}) => ({
shippingAddresses: many(shippingAddresses),
orders: many(orders),
carts: many(carts),
customerTokens: many(customerTokens),
checkouts: many(checkouts),
customerSessions: many(customerSessions),
favorites: many(favorites),
}));
export const categoriesRelations = relations(categories, ({one, many}) => ({
items: many(items),
category: one(categories, {
fields: [categories.parentId],
references: [categories.id],
relationName: "categories_parentId_categories_id"
}),
categories: many(categories, {
relationName: "categories_parentId_categories_id"
}),
}));
export const ordersRelations = relations(orders, ({one}) => ({
item: one(items, {
fields: [orders.itemId],
references: [items.id]
}),
customer: one(customers, {
fields: [orders.customerId],
references: [customers.id]
}),
checkout: one(checkouts, {
fields: [orders.checkoutId],
references: [checkouts.id]
}),
}));
export const checkoutsRelations = relations(checkouts, ({one, many}) => ({
orders: many(orders),
customer: one(customers, {
fields: [checkouts.customerId],
references: [customers.id]
}),
shippingAddress: one(shippingAddresses, {
fields: [checkouts.shippingAddressId],
references: [shippingAddresses.id]
}),
checkoutItems: many(checkoutItems),
}));
export const cartItemsRelations = relations(cartItems, ({one}) => ({
cart: one(carts, {
fields: [cartItems.cartId],
references: [carts.id]
}),
item: one(items, {
fields: [cartItems.itemId],
references: [items.id]
}),
}));
export const cartsRelations = relations(carts, ({one, many}) => ({
cartItems: many(cartItems),
customer: one(customers, {
fields: [carts.customerId],
references: [customers.id]
}),
}));
export const customerTokensRelations = relations(customerTokens, ({one}) => ({
customer: one(customers, {
fields: [customerTokens.customerId],
references: [customers.id]
}),
}));
export const customerSessionsRelations = relations(customerSessions, ({one}) => ({
customer: one(customers, {
fields: [customerSessions.customerId],
references: [customers.id]
}),
}));
export const itemTagsRelations = relations(itemTags, ({one}) => ({
item: one(items, {
fields: [itemTags.itemId],
references: [items.id]
}),
tag: one(tags, {
fields: [itemTags.tagId],
references: [tags.id]
}),
}));
export const tagsRelations = relations(tags, ({many}) => ({
itemTags: many(itemTags),
}));
export const favoritesRelations = relations(favorites, ({one}) => ({
customer: one(customers, {
fields: [favorites.customerId],
references: [customers.id]
}),
item: one(items, {
fields: [favorites.itemId],
references: [items.id]
}),
}));
export const checkoutItemsRelations = relations(checkoutItems, ({one}) => ({
checkout: one(checkouts, {
fields: [checkoutItems.checkoutId],
references: [checkouts.id]
}),
item: one(items, {
fields: [checkoutItems.itemId],
references: [items.id]
}),
}));
+289
View File
@@ -0,0 +1,289 @@
import { pgTable, serial, varchar, timestamp, text, foreignKey, integer, boolean, jsonb, index, uniqueIndex, unique, primaryKey } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
export const pgmigrations = pgTable("pgmigrations", {
id: serial().primaryKey().notNull(),
name: varchar({ length: 255 }).notNull(),
runOn: timestamp("run_on", { mode: 'string' }).notNull(),
});
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 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(),
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 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 items = pgTable("items", {
id: serial().primaryKey().notNull(),
name: text().notNull(),
description: text(),
priceCents: integer("price_cents").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 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 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 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 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 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 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 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 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 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 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 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"}),
]);
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"}),
]);
+1695 -3
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -30,6 +30,7 @@
"@types/markdown-it": "^14.2.0",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"drizzle-orm": "^0.45.2",
"express": "^4.19.2",
"express-rate-limit": "^8.6.2",
"markdown-it": "^15.0.0",
@@ -42,6 +43,8 @@
},
"devDependencies": {
"@eslint/js": "^9.39.5",
"@tinqerjs/pg-promise-adapter": "^0.0.27",
"@tinqerjs/tinqer": "^0.0.27",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
@@ -52,10 +55,12 @@
"@types/nodemailer": "^6.4.15",
"@types/pg": "^8.11.6",
"@types/supertest": "^6.0.2",
"drizzle-kit": "^0.31.10",
"eslint": "^9.39.5",
"eslint-plugin-sonarjs": "^4.2.0",
"globals": "^17.11.0",
"jest": "^29.7.0",
"pg-promise": "^12.7.1",
"supertest": "^7.0.0",
"ts-jest": "^29.2.4",
"tsx": "^4.16.5",
@@ -0,0 +1,74 @@
// Spike (#216): buildItemFilterSql expressed with Drizzle.
//
// Deliberately the hardest thing in the codebase — six optional clauses composed
// at run time, a recursive CTE for the category subtree, an ANY(...::int[]) tag
// match with a count equality, and array parameters. If this cannot be said
// cleanly, nothing else in the conversion matters.
import { SQL, and, eq, gte, lte, sql, inArray, exists } from 'drizzle-orm';
import { items, itemTags, favorites, categories } from './schema';
export interface SpikeFilters {
categoryIds: number[];
tagIds: number[];
minPriceCents: number | null;
maxPriceCents: number | null;
status: string[] | null;
favoritesOnly: boolean;
}
export function buildItemFilterDrizzle(
filters: SpikeFilters,
favoritesCustomerId: number | null
): SQL[] {
const clauses: SQL[] = [];
// The recursive CTE. Drizzle's $with() builds statement-level CTEs; this one
// has to sit inside an IN (...) subquery, so it stays a sql`` template.
//
// Note what that template does with ${filters.categoryIds}: it emits a BIND
// PARAMETER, not text. That is the difference from a plain JS template
// literal, and it is the whole of the #202 invariant expressed by the type
// system rather than by a comment — there is no way to spell "interpolate
// this value as SQL text" by accident.
if (filters.categoryIds.length) {
clauses.push(sql`${items.categoryId} IN (
WITH RECURSIVE subtree AS (
SELECT id FROM ${categories} WHERE id = ANY(${sql.param(filters.categoryIds)}::int[])
UNION ALL
SELECT c.id FROM ${categories} c JOIN subtree s ON c.parent_id = s.id
)
SELECT id FROM subtree
)`);
}
// AND, not OR: the item must carry every selected tag, so the count of
// matched rows has to equal the number requested.
if (filters.tagIds.length) {
clauses.push(sql`(
SELECT COUNT(*) FROM ${itemTags} it
WHERE it.item_id = ${items.id} AND it.tag_id = ANY(${sql.param(filters.tagIds)}::int[])
) = ${filters.tagIds.length}`);
}
if (filters.minPriceCents !== null) clauses.push(gte(items.priceCents, filters.minPriceCents));
if (filters.maxPriceCents !== null) clauses.push(lte(items.priceCents, filters.maxPriceCents));
// inArray replaces `= ANY($n::text[])`. Drizzle emits an IN list of binds.
if (filters.status !== null) clauses.push(inArray(items.status, filters.status));
if (filters.favoritesOnly) {
if (favoritesCustomerId === null) throw new Error('favorites filter requires a customer id');
clauses.push(
exists(
sql`(SELECT 1 FROM ${favorites} f WHERE f.item_id = ${items.id} AND f.customer_id = ${favoritesCustomerId})`
)
);
}
return clauses;
}
export function combine(clauses: SQL[]): SQL | undefined {
return clauses.length ? and(...clauses) : undefined;
}
+290
View File
@@ -0,0 +1,290 @@
import { pgTable, serial, varchar, timestamp, text, foreignKey, integer, boolean, jsonb, index, uniqueIndex, unique, primaryKey } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
export const pgmigrations = pgTable("pgmigrations", {
id: serial().primaryKey().notNull(),
name: varchar({ length: 255 }).notNull(),
runOn: timestamp("run_on", { mode: 'string' }).notNull(),
});
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 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(),
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 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 items = pgTable("items", {
id: serial().primaryKey().notNull(),
name: text().notNull(),
description: text(),
priceCents: integer("price_cents").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"),
conditionNote: text("condition_note"),
}, (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 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 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 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 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 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 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 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 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 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 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 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"}),
]);
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"}),
]);
+71
View File
@@ -0,0 +1,71 @@
// Spike (#216): what Tinqer can and cannot express, recorded as runnable cases.
//
// Every case is wrapped in a function rather than evaluated at module level.
// defineSelect() parses the lambda eagerly and THROWS on a shape it cannot
// handle, so a top-level call turns an unsupported query into an import-time
// crash. That is itself a finding worth keeping: the failure is a runtime throw
// at load, not a type error, so an unsupported query compiles cleanly and takes
// the process down when the module is first required.
import { createSchema, defineSelect } from '@tinqerjs/tinqer';
import { toSql } from '@tinqerjs/pg-promise-adapter';
interface Db {
items: { id: number; name: string; price_cents: number; status: string; category_id: number | null };
categories: { id: number; parent_id: number | null };
}
const schema = createSchema<Db>();
export type Outcome = { label: string; ok: boolean; sql?: string; error?: string };
function attempt(label: string, build: () => unknown, params: Record<string, unknown>): Outcome {
try {
const r = toSql(build() as never, params as never);
return { label, ok: true, sql: r.sql };
} catch (e) {
return { label, ok: false, error: (e as Error).message.split('\n')[0] };
}
}
export function runCases(): Outcome[] {
return [
// Supported.
attempt(
'compound condition',
() =>
defineSelect(schema, (q, p: { min: number; max: number }) =>
q.from('items').where((i) => i.price_cents >= p.min && i.price_cents <= p.max).select((i) => ({ id: i.id }))
),
{ min: 1, max: 2 }
),
attempt(
'array membership',
() =>
defineSelect(schema, (q, p: { statuses: string[] }) =>
q.from('items').where((i) => p.statuses.includes(i.status)).select((i) => ({ id: i.id }))
),
{ statuses: ['available', 'sold'] }
),
// NOT supported — and these are the two shapes buildItemFilterSql needs.
attempt(
'ternary (clause optional at run time)',
() =>
defineSelect(schema, (q, p: { apply: boolean; min: number }) =>
q.from('items').where((i) => (p.apply ? i.price_cents >= p.min : true)).select((i) => ({ id: i.id }))
),
{ apply: true, min: 1 }
),
attempt(
'block body with if (clause optional at run time)',
() =>
defineSelect(schema, (q, p: { apply: boolean; min: number }) => {
let x = q.from('items');
if (p.apply) x = x.where((i) => i.price_cents >= p.min);
return x.select((i) => ({ id: i.id }));
}),
{ apply: true, min: 1 }
)
];
}