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
@@ -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 }
)
];
}