The schema for the intake pipeline. A submission becomes an `items` row at status 'pending' — already invisible to every public and storefront query since #90 — with an `item_drafts` row beside it holding the submitter's note, which link it arrived through, and the fields the drafting worker will fill in later. `upload_links` stores a digest rather than a token, so a leaked database is not also a leaked set of working links, and the admin screen can show a token exactly once. `max_submissions` is nullable for "no cap", but the route will default it to a finite number: an unbounded link should be something asked for, not something that happens when nobody thought about it. `item_drafts.upload_link_id` is ON DELETE SET NULL rather than CASCADE. Deleting a link must not delete the items that arrived through it — provenance is lost, the goods are not. `items.price_cents` keeps NOT NULL and gains a default of 80.00, so an arriving item is always priced. That is the decision taken in the design review over making the column nullable: it costs the schema-level guarantee that nothing can publish at a price nobody chose, and buys not having to teach the cart, the checkout and thirteen other files about an item without a price. The protection moves into the review queue, and `price_source` exists so that queue can say whether a number came from a model, the default, or a person. The number lives in the migration rather than in configuration. Changing a default price is a rare, deliberate act that deserves a record; an environment variable would let it drift silently between environments, and a wrong default is invisible until something has already sold at it. Verified up, down and up again rather than only forwards — an irreversible migration is one that cannot be tested. Then verified by inspection rather than assumption: the default reads 8000, both tables and the state index exist, and an item inserted with no price comes back at 8000. Backend: 263 integration, 302 unit, all passing against the new schema. Ref #222
48 lines
1.9 KiB
TypeScript
Executable File
48 lines
1.9 KiB
TypeScript
Executable File
import { Pool } from 'pg';
|
|
import { runner } from 'node-pg-migrate';
|
|
import path from 'path';
|
|
|
|
export const testPool = new Pool({
|
|
host: process.env.TEST_PGHOST || 'localhost',
|
|
port: parseInt(process.env.TEST_PGPORT || '55432', 10),
|
|
user: process.env.TEST_PGUSER || 'redefined_test',
|
|
password: process.env.TEST_PGPASSWORD || 'redefined_test',
|
|
database: process.env.TEST_PGDATABASE || 'redefined_test'
|
|
});
|
|
|
|
export async function migrate(): Promise<void> {
|
|
await runner({
|
|
databaseUrl: {
|
|
host: process.env.TEST_PGHOST || 'localhost',
|
|
port: parseInt(process.env.TEST_PGPORT || '55432', 10),
|
|
user: process.env.TEST_PGUSER || 'redefined_test',
|
|
password: process.env.TEST_PGPASSWORD || 'redefined_test',
|
|
database: process.env.TEST_PGDATABASE || 'redefined_test'
|
|
},
|
|
dir: path.resolve(__dirname, '..', '..', '..', 'migrations'),
|
|
direction: 'up',
|
|
migrationsTable: 'pgmigrations',
|
|
count: Infinity
|
|
});
|
|
}
|
|
|
|
export async function resetDb(): Promise<void> {
|
|
await testPool.query(`
|
|
TRUNCATE TABLE item_drafts, upload_links, orders, checkout_items, checkouts,
|
|
shipping_addresses, cart_items, carts, customer_tokens, customer_sessions, favorites,
|
|
customers, item_tags, item_images, items, tags, categories
|
|
RESTART IDENTITY CASCADE
|
|
`);
|
|
|
|
// admin_settings is not truncated — it holds the seeded cart_expiry_hours
|
|
// default that other suites read. But the email template rows in it are test
|
|
// data like any other, and a stored template outliving the suite that wrote
|
|
// it silently changes the mail every later suite asserts on. That is not
|
|
// hypothetical: a subject of "Gone" written by the template tests reached the
|
|
// favorite-alert tests and made five of them fail somewhere else entirely.
|
|
await testPool.query(`DELETE FROM admin_settings WHERE key LIKE 'email\_%'`);
|
|
}
|
|
|
|
export async function closeDb(): Promise<void> {
|
|
await testPool.end();
|
|
} |