Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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
Test User committed on May 15, 2026Parent: 0dc3291
1 file changed+35339d06598a3d6f44bd0532ad95dcb8dbece8e33a90
1 changed file+35−33
Modifiedscripts/emergency-pat.ts+35−33View fileUnifiedSplit
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));
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 );
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