exports.up = (pgm) => { pgm.sql(` CREATE TABLE IF NOT EXISTS upload_links ( id SERIAL PRIMARY KEY, label TEXT NOT NULL, -- The token itself is never stored, only its digest. A leaked database -- is then not also a leaked set of working upload links, and the admin -- screen can show a token exactly once — at creation — for the same -- reason a password reset link is not re-readable. token_hash TEXT NOT NULL UNIQUE, revoked_at TIMESTAMPTZ, submission_count INTEGER NOT NULL DEFAULT 0, -- Null means no cap. A link handed to a regular contributor is -- open-ended; one handed out for a single box of stock is not. The -- route defaults this to a finite number rather than null, so an -- unbounded link is something asked for rather than something that -- happens when nobody thought about it. max_submissions INTEGER, last_used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS item_drafts ( id SERIAL PRIMARY KEY, item_id INTEGER NOT NULL UNIQUE REFERENCES items(id) ON DELETE CASCADE, -- SET NULL rather than CASCADE: deleting a link must not delete the -- items that arrived through it. Provenance is lost; the goods are not. upload_link_id INTEGER REFERENCES upload_links(id) ON DELETE SET NULL, submitter_note TEXT, state TEXT NOT NULL DEFAULT 'queued', attempts INTEGER NOT NULL DEFAULT 0, model TEXT, ai_name TEXT, ai_description TEXT, ai_category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL, ai_tag_names TEXT[], -- Kept even though a suggestion is also copied onto the item, so what -- the model proposed stays readable after the admin has edited the -- item's price. Without it there is no way to ask later whether the -- model's numbers were any good. ai_suggested_price_cents INTEGER, -- ai | default | admin. Where the item's current price came from. -- Recorded rather than inferred: a model that happens to suggest exactly -- 8000, or an admin who deliberately types the model's number, both -- collapse any comparison-based guess. price_source TEXT NOT NULL DEFAULT 'default', ai_error TEXT, input_tokens INTEGER, output_tokens INTEGER, cost_micros INTEGER, drafted_at TIMESTAMPTZ, reviewed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- The review queue reads by state; everything else reads by item, which -- the UNIQUE constraint on item_id already indexes. CREATE INDEX IF NOT EXISTS item_drafts_state_idx ON item_drafts (state); -- A submitted item is priced on arrival rather than left unpriced, so the -- column keeps NOT NULL and only gains a fallback. 80.00 applies when -- nothing else supplies a price; the drafting worker in #223 writes a -- model's suggestion over it when there is one. -- -- The number lives here rather than in configuration deliberately. -- 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. ALTER TABLE items ALTER COLUMN price_cents SET DEFAULT 8000; `); }; exports.down = (pgm) => { pgm.sql(` ALTER TABLE items ALTER COLUMN price_cents DROP DEFAULT; DROP TABLE IF EXISTS item_drafts; DROP TABLE IF EXISTS upload_links; `); };