137 lines
5.0 KiB
TypeScript
137 lines
5.0 KiB
TypeScript
import { drizzle } from "drizzle-orm/node-postgres";
|
|
import { eq } from "drizzle-orm";
|
|
import { users, customSymbols, User, NewUser, CustomSymbol, NewCustomSymbol } from "../shared/schema";
|
|
import { Pool } from "pg";
|
|
|
|
// PostgreSQL connection (local on production droplet)
|
|
const pool = new Pool({
|
|
host: process.env.DB_HOST || "127.0.0.1",
|
|
port: parseInt(process.env.DB_PORT || "5432"),
|
|
user: process.env.DB_USER || "gn_admin",
|
|
password: process.env.DB_PASSWORD || "",
|
|
database: process.env.DB_NAME || "gammanexus",
|
|
ssl: (process.env.DB_SSL === "true" && process.env.NODE_ENV === "production") ? { rejectUnauthorized: false } : false,
|
|
max: 20,
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 5000,
|
|
});
|
|
|
|
export const db = drizzle(pool);
|
|
|
|
// Use raw pool client for DDL — Drizzle wraps queries in prepared statements
|
|
// and PostgreSQL rejects CREATE/ALTER IF NOT EXISTS in prepared queries.
|
|
async function executeDDL(query: string): Promise<void> {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query(query);
|
|
} catch {
|
|
// Table/column already exists — ignore silently
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// Initialize users table if it doesn't exist (called at startup)
|
|
export async function initDb(): Promise<void> {
|
|
await executeDDL(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
verification_token TEXT,
|
|
verification_token_expiry TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
|
|
// Add columns if they don't exist (for existing tables)
|
|
await executeDDL(`ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE`);
|
|
await executeDDL(`ALTER TABLE users ADD COLUMN verification_token TEXT`);
|
|
await executeDDL(`ALTER TABLE users ADD COLUMN verification_token_expiry TIMESTAMP WITH TIME ZONE`);
|
|
|
|
// Create custom_symbols table
|
|
await executeDDL(`
|
|
CREATE TABLE IF NOT EXISTS custom_symbols (
|
|
id SERIAL PRIMARY KEY,
|
|
ticker TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'etf',
|
|
sector TEXT,
|
|
user_id TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
}
|
|
|
|
export async function createUser(data: NewUser): Promise<User> {
|
|
const [user] = await db.insert(users).values(data).returning();
|
|
return user;
|
|
}
|
|
|
|
export async function getUserByEmail(email: string): Promise<User | undefined> {
|
|
const result = await db.select().from(users).where(eq(users.email, email.toLowerCase()));
|
|
return result[0];
|
|
}
|
|
|
|
export async function getUserById(id: number): Promise<User | undefined> {
|
|
const result = await db.select().from(users).where(eq(users.id, id));
|
|
return result[0];
|
|
}
|
|
|
|
export async function updateUser(id: number, data: Partial<Pick<User, "name" | "email">>): Promise<User> {
|
|
const result = await db.update(users).set(data).where(eq(users.id, id)).returning();
|
|
return result[0];
|
|
}
|
|
|
|
export async function updateUserPassword(id: number, passwordHash: string): Promise<User> {
|
|
const result = await db.update(users).set({ passwordHash }).where(eq(users.id, id)).returning();
|
|
return result[0];
|
|
}
|
|
|
|
export async function updateUserVerification(id: number, token: string, expiry: Date): Promise<void> {
|
|
await db.update(users).set({ verificationToken: token, verificationTokenExpiry: expiry }).where(eq(users.id, id));
|
|
}
|
|
|
|
export async function getUserByVerificationToken(token: string): Promise<User | undefined> {
|
|
const { and, eq, gt } = await import("drizzle-orm");
|
|
const result = await db.select().from(users).where(
|
|
and(
|
|
eq(users.verificationToken, token),
|
|
gt(users.verificationTokenExpiry, new Date())
|
|
)
|
|
);
|
|
return result[0];
|
|
}
|
|
|
|
export async function markEmailVerified(id: number): Promise<void> {
|
|
await db.update(users).set({ emailVerified: true, verificationToken: null, verificationTokenExpiry: null }).where(eq(users.id, id));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Custom Symbols CRUD
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function getAllCustomSymbols(): Promise<CustomSymbol[]> {
|
|
return await db.select().from(customSymbols);
|
|
}
|
|
|
|
export async function getCustomSymbolByTicker(ticker: string): Promise<CustomSymbol | undefined> {
|
|
const result = await db.select().from(customSymbols).where(eq(customSymbols.ticker, ticker.toUpperCase()));
|
|
return result[0];
|
|
}
|
|
|
|
export async function createCustomSymbol(data: NewCustomSymbol): Promise<CustomSymbol> {
|
|
const [symbol] = await db.insert(customSymbols).values({
|
|
...data,
|
|
ticker: data.ticker?.toUpperCase() || data.ticker,
|
|
}).returning();
|
|
return symbol;
|
|
}
|
|
|
|
export async function deleteCustomSymbol(ticker: string): Promise<boolean> {
|
|
const result = await db.delete(customSymbols).where(eq(customSymbols.ticker, ticker.toUpperCase())).returning();
|
|
return result.length > 0;
|
|
}
|