Commit9d06598unknown_key
fix(scripts): emergency-pat — use raw SQL to survive schema drift
fix(scripts): emergency-pat — use raw SQL to survive schema drift When the live site is broken AND production DB is missing recent migrations (common when the deploy that ships the migration is itself the deploy you're trying to push), drizzle's `select()` blows up because it lists every column in the local schema and production rejects with `column "X" does not exist`. Switching to @neondatabase/serverless raw SQL so we only touch the columns we name. Schema drift no longer matters — we always read just id + username + is_admin, and only insert into known api_tokens columns. https://claude.ai/code/session_01QFLWDxWw65DX6enMcS5Lwe
1 file changed+35−339d06598a3d6f44bd0532ad95dcb8dbece8e33a90
1 changed file+35−33
Modifiedscripts/emergency-pat.ts+35−33View fileUnifiedSplit
@@ -6,17 +6,15 @@
66// the production database via DATABASE_URL.
77//
88// Run: bun run scripts/emergency-pat.ts
9// Required env: DATABASE_URL (from your local .env)
9// Required env: DATABASE_URL (from your local .env, or set inline)
1010//
11// The script:
12// 1. Loads .env
13// 2. Finds the first admin user (or a user matching $EMERGENCY_PAT_USER)
14// 3. Inserts a fresh PAT with `admin` scope
15// 4. Prints the raw token — you only see it once, so copy it now
11// Uses RAW SQL via @neondatabase/serverless rather than drizzle's
12// schema layer. Reason: when this script is needed, production is
13// usually missing recent migrations, so drizzle's SELECT (which lists
14// every schema column) blows up on missing columns. Raw SQL only
15// touches the columns we name, so schema drift doesn't matter.
1616
17import { db } from "../src/db/index";
18import { users, apiTokens } from "../src/db/schema";
19import { eq } from "drizzle-orm";
17import { neon } from "@neondatabase/serverless";
2018
2119function generateToken(): string {
2220 const bytes = crypto.getRandomValues(new Uint8Array(32));
@@ -37,26 +35,31 @@ async function hashToken(token: string): Promise<string> {
3735}
3836
3937async function main() {
38 const url = process.env.DATABASE_URL;
39 if (!url) {
40 console.error("DATABASE_URL is not set.");
41 process.exit(1);
42 }
43 const sql = neon(url);
44
4045 const requestedUser = process.env.EMERGENCY_PAT_USER?.trim();
41 let user: typeof users.$inferSelect | undefined;
4246
47 let userRow: { id: string; username: string } | undefined;
4348 if (requestedUser) {
44 [user] = await db
45 .select()
46 .from(users)
47 .where(eq(users.username, requestedUser))
48 .limit(1);
49 if (!user) {
49 const rows = (await sql`
50 SELECT id, username FROM users WHERE username = ${requestedUser} LIMIT 1
51 `) as Array<{ id: string; username: string }>;
52 userRow = rows[0];
53 if (!userRow) {
5054 console.error(`No user with username "${requestedUser}".`);
5155 process.exit(1);
5256 }
5357 } else {
54 [user] = await db
55 .select()
56 .from(users)
57 .where(eq(users.isAdmin, true))
58 .limit(1);
59 if (!user) {
58 const rows = (await sql`
59 SELECT id, username FROM users WHERE is_admin = true ORDER BY created_at ASC LIMIT 1
60 `) as Array<{ id: string; username: string }>;
61 userRow = rows[0];
62 if (!userRow) {
6063 console.error(
6164 "No admin user found. Set EMERGENCY_PAT_USER=<username> and rerun."
6265 );
@@ -66,21 +69,20 @@ async function main() {
6669
6770 const token = generateToken();
6871 const tokenHash = await hashToken(token);
69 await db.insert(apiTokens).values({
70 userId: user.id,
71 name: "emergency-deploy-fix",
72 tokenHash,
73 tokenPrefix: token.slice(0, 12),
74 scopes: "admin",
75 });
72 const tokenPrefix = token.slice(0, 12);
7673
77 console.log(`User: ${user.username} (id ${user.id})`);
74 await sql`
75 INSERT INTO api_tokens (user_id, name, token_hash, token_prefix, scopes)
76 VALUES (${userRow.id}, 'emergency-deploy-fix', ${tokenHash}, ${tokenPrefix}, 'admin')
77 `;
78
79 console.log("");
80 console.log(`User: ${userRow.username}`);
7881 console.log(`Token: ${token}`);
7982 console.log("");
80 console.log("Copy the token above NOW — it is hashed in the DB and");
81 console.log("cannot be recovered later. Then push with:");
83 console.log("Copy the token NOW (only shown once). Then run these two:");
8284 console.log("");
83 console.log(` git remote set-url gluecron "https://${user.username}:${token}@gluecron.com/ccantynz/Gluecron.com.git"`);
85 console.log(` git remote set-url gluecron "https://${userRow.username}:${token}@gluecron.com/ccantynz/Gluecron.com.git"`);
8486 console.log(" git push gluecron main");
8587 process.exit(0);
8688}
8789