Add PostgreSQL support with SQLite/Postgres database compatibility layer
- Add dbGet/dbAll helper functions for database-agnostic queries - Add toDbBool/convertBooleansForDb for boolean type conversion - Add toDbDate/getNow for timestamp type handling - Add generateId that returns UUID for Postgres, nanoid for SQLite - Update all routes to use compatibility helpers - Add normalizeEvent to return clean number types from Postgres decimal - Add formatPrice utility for consistent price display - Add legal pages admin interface with RichTextEditor - Update carousel images - Add drizzle migration files for PostgreSQL
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3';
|
||||
import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';
|
||||
import Database from 'better-sqlite3';
|
||||
@@ -29,5 +30,51 @@ if (dbType === 'postgres') {
|
||||
db = drizzleSqlite(sqlite, { schema });
|
||||
}
|
||||
|
||||
export { db };
|
||||
// ==================== Database Compatibility Helpers ====================
|
||||
// These functions abstract the differences between SQLite and PostgreSQL Drizzle drivers:
|
||||
// - SQLite uses .get() for single result, .all() for multiple
|
||||
// - PostgreSQL returns arrays directly (no .get()/.all() methods)
|
||||
|
||||
/**
|
||||
* Get a single result from a query (works with both SQLite and PostgreSQL)
|
||||
* @param query - A Drizzle query builder (e.g., db.select().from(table).where(...))
|
||||
* @returns The first result or null
|
||||
*/
|
||||
export async function dbGet<T>(query: any): Promise<T | null> {
|
||||
if (dbType === 'postgres') {
|
||||
const results = await query;
|
||||
return results[0] || null;
|
||||
}
|
||||
// SQLite - use .get()
|
||||
return query.get() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all results from a query (works with both SQLite and PostgreSQL)
|
||||
* @param query - A Drizzle query builder (e.g., db.select().from(table).where(...))
|
||||
* @returns Array of results
|
||||
*/
|
||||
export async function dbAll<T>(query: any): Promise<T[]> {
|
||||
if (dbType === 'postgres') {
|
||||
return await query;
|
||||
}
|
||||
// SQLite - use .all()
|
||||
return query.all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if using PostgreSQL
|
||||
*/
|
||||
export function isPostgres(): boolean {
|
||||
return dbType === 'postgres';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if using SQLite
|
||||
*/
|
||||
export function isSqlite(): boolean {
|
||||
return dbType === 'sqlite';
|
||||
}
|
||||
|
||||
export { db, dbType };
|
||||
export * from './schema.js';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'dotenv/config';
|
||||
import { db } from './index.js';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
const dbType = process.env.DB_TYPE || 'sqlite';
|
||||
console.log(`Database type: ${dbType}`);
|
||||
console.log(`Database URL: ${process.env.DATABASE_URL?.substring(0, 30)}...`);
|
||||
|
||||
async function migrate() {
|
||||
console.log('Running migrations...');
|
||||
@@ -384,6 +387,23 @@ async function migrate() {
|
||||
updated_by TEXT REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Legal pages table for admin-editable legal content
|
||||
await (db as any).run(sql`
|
||||
CREATE TABLE IF NOT EXISTS legal_pages (
|
||||
id TEXT PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
title_es TEXT,
|
||||
content_text TEXT NOT NULL,
|
||||
content_text_es TEXT,
|
||||
content_markdown TEXT NOT NULL,
|
||||
content_markdown_es TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
updated_by TEXT REFERENCES users(id),
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
} else {
|
||||
// PostgreSQL migrations
|
||||
await (db as any).execute(sql`
|
||||
@@ -716,6 +736,23 @@ async function migrate() {
|
||||
updated_by UUID REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Legal pages table for admin-editable legal content
|
||||
await (db as any).execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS legal_pages (
|
||||
id UUID PRIMARY KEY,
|
||||
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
title_es VARCHAR(255),
|
||||
content_text TEXT NOT NULL,
|
||||
content_text_es TEXT,
|
||||
content_markdown TEXT NOT NULL,
|
||||
content_markdown_es TEXT,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
updated_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
console.log('Migrations completed successfully!');
|
||||
|
||||
@@ -249,6 +249,21 @@ export const sqliteEmailSettings = sqliteTable('email_settings', {
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
});
|
||||
|
||||
// Legal Pages table for admin-editable legal content
|
||||
export const sqliteLegalPages = sqliteTable('legal_pages', {
|
||||
id: text('id').primaryKey(),
|
||||
slug: text('slug').notNull().unique(),
|
||||
title: text('title').notNull(), // English title
|
||||
titleEs: text('title_es'), // Spanish title
|
||||
contentText: text('content_text').notNull(), // Plain text edited by admin (English)
|
||||
contentTextEs: text('content_text_es'), // Plain text edited by admin (Spanish)
|
||||
contentMarkdown: text('content_markdown').notNull(), // Generated markdown for public display (English)
|
||||
contentMarkdownEs: text('content_markdown_es'), // Generated markdown for public display (Spanish)
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
updatedBy: text('updated_by').references(() => sqliteUsers.id),
|
||||
createdAt: text('created_at').notNull(),
|
||||
});
|
||||
|
||||
// Site Settings table for global website configuration
|
||||
export const sqliteSiteSettings = sqliteTable('site_settings', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -512,6 +527,21 @@ export const pgEmailSettings = pgTable('email_settings', {
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
});
|
||||
|
||||
// Legal Pages table for admin-editable legal content
|
||||
export const pgLegalPages = pgTable('legal_pages', {
|
||||
id: uuid('id').primaryKey(),
|
||||
slug: varchar('slug', { length: 100 }).notNull().unique(),
|
||||
title: varchar('title', { length: 255 }).notNull(), // English title
|
||||
titleEs: varchar('title_es', { length: 255 }), // Spanish title
|
||||
contentText: pgText('content_text').notNull(), // Plain text edited by admin (English)
|
||||
contentTextEs: pgText('content_text_es'), // Plain text edited by admin (Spanish)
|
||||
contentMarkdown: pgText('content_markdown').notNull(), // Generated markdown for public display (English)
|
||||
contentMarkdownEs: pgText('content_markdown_es'), // Generated markdown for public display (Spanish)
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
updatedBy: uuid('updated_by').references(() => pgUsers.id),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
});
|
||||
|
||||
// Site Settings table for global website configuration
|
||||
export const pgSiteSettings = pgTable('site_settings', {
|
||||
id: uuid('id').primaryKey(),
|
||||
@@ -556,6 +586,7 @@ export const magicLinkTokens = dbType === 'postgres' ? pgMagicLinkTokens : sqlit
|
||||
export const userSessions = dbType === 'postgres' ? pgUserSessions : sqliteUserSessions;
|
||||
export const invoices = dbType === 'postgres' ? pgInvoices : sqliteInvoices;
|
||||
export const siteSettings = dbType === 'postgres' ? pgSiteSettings : sqliteSiteSettings;
|
||||
export const legalPages = dbType === 'postgres' ? pgLegalPages : sqliteLegalPages;
|
||||
|
||||
// Type exports
|
||||
export type User = typeof sqliteUsers.$inferSelect;
|
||||
@@ -584,3 +615,5 @@ export type Invoice = typeof sqliteInvoices.$inferSelect;
|
||||
export type NewInvoice = typeof sqliteInvoices.$inferInsert;
|
||||
export type SiteSettings = typeof sqliteSiteSettings.$inferSelect;
|
||||
export type NewSiteSettings = typeof sqliteSiteSettings.$inferInsert;
|
||||
export type LegalPage = typeof sqliteLegalPages.$inferSelect;
|
||||
export type NewLegalPage = typeof sqliteLegalPages.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user