fix: update schema to fix multiple primary key issues and switch to node-postgres
- Replace primaryKey constraints with uniqueIndex where needed - Fix schema for Auth, Contacts, Exchange Rates, Expense Participants, and Accounts tables - Add missing import for uniqueIndex - Switch from postgres.js to node-postgres (pg) for Drizzle - Add proper type safety with TypeScript - Add graceful shutdown of database connections
This commit is contained in:
parent
74b7ddf3d6
commit
f9d5f8b404
101
README.md
Normal file
101
README.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Splitwise Application
|
||||
|
||||
A Splitwise-style expense-splitting application with double-entry bookkeeping and multi-currency support.
|
||||
|
||||
## Getting Started with Docker
|
||||
|
||||
The application is containerized using Docker, making it easy to set up and run locally.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
### Running the Application
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd splitwise
|
||||
```
|
||||
|
||||
2. Start the database and backend services:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
3. Generate and run database migrations:
|
||||
```bash
|
||||
# Generate migrations from schema
|
||||
docker-compose run backend bun run db:generate
|
||||
|
||||
# Run migrations
|
||||
docker-compose run --profile migration migration
|
||||
```
|
||||
|
||||
4. Access the application:
|
||||
- Backend API: http://localhost:3000
|
||||
- Drizzle Studio (database UI): http://localhost:4000
|
||||
|
||||
### Useful Commands
|
||||
|
||||
#### Start specific services:
|
||||
```bash
|
||||
# Start only the database
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Start Drizzle Studio for database management
|
||||
docker-compose --profile studio up drizzle-studio
|
||||
|
||||
# Run migrations
|
||||
docker-compose --profile migration up migration
|
||||
```
|
||||
|
||||
#### View logs:
|
||||
```bash
|
||||
# All services
|
||||
docker-compose logs -f
|
||||
|
||||
# Specific service
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### Rebuild containers:
|
||||
```bash
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
#### Stop all services:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### Reset everything (including database volume):
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `/backend` - Backend API built with Bun, Hono, and Drizzle ORM
|
||||
- `/frontend` - Frontend application (to be implemented)
|
||||
- `/docs` - Project documentation
|
||||
|
||||
### Tech Stack
|
||||
|
||||
#### Backend
|
||||
- **Runtime**: Bun
|
||||
- **Framework**: Hono (TypeScript-first web framework)
|
||||
- **Database**: PostgreSQL
|
||||
- **ORM**: Drizzle
|
||||
- **Authentication**: better-auth
|
||||
- **Validation**: Zod
|
||||
|
||||
#### Frontend (planned)
|
||||
- **Framework**: React with TypeScript
|
||||
- **Build Tool**: Vite
|
||||
- **Styling**: TailwindCSS with Shadcn UI
|
||||
- **State Management**: Zustand
|
||||
- **Data Fetching**: TanStack Query
|
5
backend/.env.example
Normal file
5
backend/.env.example
Normal file
@ -0,0 +1,5 @@
|
||||
PORT=3000
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/splitwise
|
||||
JWT_SECRET=your-jwt-secret-key-here
|
||||
NODE_ENV=development
|
||||
DATABASE_SSL=false
|
38
backend/Dockerfile
Normal file
38
backend/Dockerfile
Normal file
@ -0,0 +1,38 @@
|
||||
FROM oven/bun:1.2 as base
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lock ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["bun", "run", "start"]
|
||||
|
||||
# Development stage
|
||||
FROM base as development
|
||||
|
||||
# Override environment variables for development
|
||||
ENV NODE_ENV=development
|
||||
|
||||
# Start development server
|
||||
CMD ["bun", "run", "dev"]
|
||||
|
||||
# Migration stage
|
||||
FROM base as migration
|
||||
|
||||
# Run migrations
|
||||
CMD ["bun", "run", "db:migrate"]
|
@ -8,7 +8,6 @@
|
||||
"drizzle-orm": "^0.30.5",
|
||||
"hono": "^4.2.4",
|
||||
"pg": "^8.11.3",
|
||||
"postgres": "^3.4.3",
|
||||
"zod": "^3.22.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -15,7 +15,6 @@
|
||||
"drizzle-orm": "^0.30.5",
|
||||
"hono": "^4.2.4",
|
||||
"pg": "^8.11.3",
|
||||
"postgres": "^3.4.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,19 +1,60 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
||||
import { Pool } from 'pg';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Get database connection string from environment variable or use default
|
||||
const connectionString = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/splitwise';
|
||||
|
||||
// Create a PostgreSQL client
|
||||
const client = postgres(connectionString, { max: 10 });
|
||||
// Configure connection options
|
||||
const poolConfig = {
|
||||
connectionString,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 10000,
|
||||
ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : undefined,
|
||||
};
|
||||
|
||||
// Create a PostgreSQL connection pool
|
||||
const pool = new Pool(poolConfig);
|
||||
|
||||
// Add error handler for unexpected errors
|
||||
pool.on('error', (err) => {
|
||||
console.error('Unexpected database error:', err);
|
||||
});
|
||||
|
||||
// Create a Drizzle ORM instance
|
||||
export const db = drizzle(client);
|
||||
export const db = drizzle(pool);
|
||||
|
||||
// Function to ensure migrations directory exists
|
||||
function ensureMigrationsDir() {
|
||||
const migrationsFolder = './src/db/migrations';
|
||||
const metaFolder = path.join(migrationsFolder, 'meta');
|
||||
|
||||
// Create migrations directory if it doesn't exist
|
||||
if (!fs.existsSync(migrationsFolder)) {
|
||||
fs.mkdirSync(migrationsFolder, { recursive: true });
|
||||
}
|
||||
|
||||
// Create meta directory if it doesn't exist
|
||||
if (!fs.existsSync(metaFolder)) {
|
||||
fs.mkdirSync(metaFolder, { recursive: true });
|
||||
}
|
||||
|
||||
// Create an empty journal file if it doesn't exist
|
||||
const journalPath = path.join(metaFolder, '_journal.json');
|
||||
if (!fs.existsSync(journalPath)) {
|
||||
fs.writeFileSync(journalPath, JSON.stringify({ entries: [] }));
|
||||
}
|
||||
}
|
||||
|
||||
// Function to run migrations
|
||||
export async function runMigrations() {
|
||||
try {
|
||||
console.log('Ensuring migrations directory exists...');
|
||||
ensureMigrationsDir();
|
||||
|
||||
console.log('Running migrations...');
|
||||
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
||||
console.log('Migrations completed successfully');
|
||||
@ -21,4 +62,9 @@ export async function runMigrations() {
|
||||
console.error('Migration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to close the database connection
|
||||
export async function closeDb() {
|
||||
await pool.end();
|
||||
}
|
252
backend/src/db/migrations/0000_married_pestilence.sql
Normal file
252
backend/src/db/migrations/0000_married_pestilence.sql
Normal file
@ -0,0 +1,252 @@
|
||||
CREATE TABLE IF NOT EXISTS "accounts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"type" varchar(20) NOT NULL,
|
||||
"currency_code" varchar(3) NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "accounts_user_id_currency_code_pk" PRIMARY KEY("user_id","currency_code")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "currencies" (
|
||||
"code" varchar(3) PRIMARY KEY NOT NULL,
|
||||
"name" varchar(100) NOT NULL,
|
||||
"symbol" varchar(10) NOT NULL,
|
||||
"decimal_digits" integer DEFAULT 2 NOT NULL,
|
||||
"is_active" boolean DEFAULT true NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "exchange_rates" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"from_currency_code" varchar(3) NOT NULL,
|
||||
"to_currency_code" varchar(3) NOT NULL,
|
||||
"rate" varchar(24) NOT NULL,
|
||||
"effective_date" timestamp with time zone NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "exchange_rates_from_currency_code_to_currency_code_effective_date_pk" PRIMARY KEY("from_currency_code","to_currency_code","effective_date")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "expense_participants" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"expense_id" uuid NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"share_amount" numeric(20, 2) NOT NULL,
|
||||
"share_percentage" numeric(5, 2),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "expense_participants_expense_id_user_id_pk" PRIMARY KEY("expense_id","user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "expenses" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"amount" numeric(20, 2) NOT NULL,
|
||||
"currency_code" varchar(3) NOT NULL,
|
||||
"date" timestamp with time zone NOT NULL,
|
||||
"payer_id" uuid NOT NULL,
|
||||
"receipt_image_url" text,
|
||||
"created_by" uuid NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "auth" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"provider" varchar(50) NOT NULL,
|
||||
"provider_user_id" varchar(255),
|
||||
"email" varchar(255) NOT NULL,
|
||||
"password_hash" text,
|
||||
"refresh_token" text,
|
||||
"last_login" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "auth_provider_provider_user_id_pk" PRIMARY KEY("provider","provider_user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "contacts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"contact_id" uuid NOT NULL,
|
||||
"status" varchar(20) DEFAULT 'pending' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "contacts_user_id_contact_id_pk" PRIMARY KEY("user_id","contact_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "notifications" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"type" varchar(50) NOT NULL,
|
||||
"content" jsonb NOT NULL,
|
||||
"is_read" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "settlements" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"payer_id" uuid NOT NULL,
|
||||
"receiver_id" uuid NOT NULL,
|
||||
"amount" numeric(20, 2) NOT NULL,
|
||||
"currency_code" varchar(3) NOT NULL,
|
||||
"date" timestamp with time zone NOT NULL,
|
||||
"memo" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "transactions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"expense_id" uuid,
|
||||
"settlement_id" uuid,
|
||||
"from_account_id" uuid NOT NULL,
|
||||
"to_account_id" uuid NOT NULL,
|
||||
"amount" numeric(20, 2) NOT NULL,
|
||||
"currency_code" varchar(3) NOT NULL,
|
||||
"exchange_rate" numeric(24, 12) DEFAULT '1' NOT NULL,
|
||||
"date" timestamp with time zone NOT NULL,
|
||||
"type" varchar(20) NOT NULL,
|
||||
"memo" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"display_name" varchar(255) NOT NULL,
|
||||
"profile_photo_url" text,
|
||||
"default_currency_code" varchar(3) DEFAULT 'USD' NOT NULL,
|
||||
"locale" varchar(10) DEFAULT 'en-US' NOT NULL,
|
||||
"timezone" varchar(50) DEFAULT 'UTC' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "exchange_rates" ADD CONSTRAINT "exchange_rates_from_currency_code_currencies_code_fk" FOREIGN KEY ("from_currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "exchange_rates" ADD CONSTRAINT "exchange_rates_to_currency_code_currencies_code_fk" FOREIGN KEY ("to_currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "expense_participants" ADD CONSTRAINT "expense_participants_expense_id_expenses_id_fk" FOREIGN KEY ("expense_id") REFERENCES "expenses"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "expense_participants" ADD CONSTRAINT "expense_participants_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_payer_id_users_id_fk" FOREIGN KEY ("payer_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "auth" ADD CONSTRAINT "auth_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_contact_id_users_id_fk" FOREIGN KEY ("contact_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_payer_id_users_id_fk" FOREIGN KEY ("payer_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_receiver_id_users_id_fk" FOREIGN KEY ("receiver_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_expense_id_expenses_id_fk" FOREIGN KEY ("expense_id") REFERENCES "expenses"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_settlement_id_settlements_id_fk" FOREIGN KEY ("settlement_id") REFERENCES "settlements"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_from_account_id_accounts_id_fk" FOREIGN KEY ("from_account_id") REFERENCES "accounts"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_to_account_id_accounts_id_fk" FOREIGN KEY ("to_account_id") REFERENCES "accounts"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
1020
backend/src/db/migrations/meta/0000_snapshot.json
Normal file
1020
backend/src/db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
13
backend/src/db/migrations/meta/_journal.json
Normal file
13
backend/src/db/migrations/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1748383049113,
|
||||
"tag": "0000_married_pestilence",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { pgTable, varchar, integer, boolean, uuid, timestamp, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, varchar, integer, boolean, uuid, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
|
||||
// Currencies table
|
||||
@ -20,7 +20,7 @@ export const exchangeRates = pgTable('exchange_rates', {
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => {
|
||||
return {
|
||||
currencyPairDateIdx: primaryKey({ columns: [table.fromCurrencyCode, table.toCurrencyCode, table.effectiveDate] }),
|
||||
currencyPairDateIdx: uniqueIndex('currency_pair_date_idx').on(table.fromCurrencyCode, table.toCurrencyCode, table.effectiveDate),
|
||||
};
|
||||
});
|
||||
|
||||
@ -34,6 +34,6 @@ export const accounts = pgTable('accounts', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => {
|
||||
return {
|
||||
userCurrencyUniqueIdx: primaryKey({ columns: [table.userId, table.currencyCode] }),
|
||||
userCurrencyUniqueIdx: uniqueIndex('user_currency_unique_idx').on(table.userId, table.currencyCode),
|
||||
};
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { pgTable, uuid, varchar, text, timestamp, decimal, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, varchar, text, timestamp, decimal, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
import { currencies } from './currencies';
|
||||
|
||||
@ -28,6 +28,6 @@ export const expenseParticipants = pgTable('expense_participants', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => {
|
||||
return {
|
||||
expenseUserUniqueIdx: primaryKey({ columns: [table.expenseId, table.userId] }),
|
||||
expenseUserUniqueIdx: uniqueIndex('expense_user_unique_idx').on(table.expenseId, table.userId),
|
||||
};
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { pgTable, uuid, varchar, text, timestamp, primaryKey } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, varchar, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
// Users table
|
||||
export const users = pgTable('users', {
|
||||
@ -27,7 +27,7 @@ export const auth = pgTable('auth', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => {
|
||||
return {
|
||||
providerUniqueIdx: primaryKey({ columns: [table.provider, table.providerUserId] }),
|
||||
providerUniqueIdx: uniqueIndex('provider_user_id_unique_idx').on(table.provider, table.providerUserId),
|
||||
};
|
||||
});
|
||||
|
||||
@ -41,6 +41,6 @@ export const contacts = pgTable('contacts', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => {
|
||||
return {
|
||||
userContactUniqueIdx: primaryKey({ columns: [table.userId, table.contactId] }),
|
||||
userContactUniqueIdx: uniqueIndex('user_contact_unique_idx').on(table.userId, table.contactId),
|
||||
};
|
||||
});
|
@ -3,7 +3,9 @@ import { logger } from 'hono/logger';
|
||||
import { cors } from 'hono/cors';
|
||||
import { secureHeaders } from 'hono/secure-headers';
|
||||
import { prettyJSON } from 'hono/pretty-json';
|
||||
import { db, runMigrations } from './db';
|
||||
import { db, runMigrations, closeDb } from './db';
|
||||
import { AppEnv } from './types/hono';
|
||||
import { dbMiddleware } from './middleware/db';
|
||||
|
||||
// Import API routes (will be created later)
|
||||
// import { authRoutes } from './controllers/auth';
|
||||
@ -13,8 +15,8 @@ import { db, runMigrations } from './db';
|
||||
// Define port from environment or default
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Create a new Hono app
|
||||
const app = new Hono();
|
||||
// Create a new Hono app with typed environment
|
||||
const app = new Hono<AppEnv>();
|
||||
|
||||
// Initialize the database
|
||||
const initDb = async () => {
|
||||
@ -33,18 +35,22 @@ app.use('*', cors());
|
||||
app.use('*', secureHeaders());
|
||||
app.use('*', prettyJSON());
|
||||
|
||||
// Add db to context
|
||||
app.use('*', async (c, next) => {
|
||||
c.set('db', db);
|
||||
await next();
|
||||
});
|
||||
// Add db to context with proper type checking
|
||||
app.use('*', dbMiddleware);
|
||||
|
||||
// Health check route
|
||||
app.get('/', (c) => {
|
||||
app.get('/', async (c) => {
|
||||
// We can safely access the db from the context with type information
|
||||
const db = c.get('db');
|
||||
|
||||
// We'll just check if the database connection is working
|
||||
// but won't actually run a query for the health check
|
||||
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
message: 'Splitwise API is running',
|
||||
version: '1.0.0',
|
||||
database: 'Connected', // In a real implementation, we'd check db connectivity
|
||||
});
|
||||
});
|
||||
|
||||
@ -73,6 +79,19 @@ app.notFound((c) => {
|
||||
// Initialize database and start the server
|
||||
initDb().catch(console.error);
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Gracefully shutting down...');
|
||||
await closeDb();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('Gracefully shutting down...');
|
||||
await closeDb();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
export default {
|
||||
port: PORT,
|
||||
|
12
backend/src/middleware/db.ts
Normal file
12
backend/src/middleware/db.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
import { db } from '../db';
|
||||
import type { AppEnv } from '../types/hono';
|
||||
|
||||
/**
|
||||
* Middleware to add the database to the request context.
|
||||
* This middleware is properly typed with the AppEnv interface.
|
||||
*/
|
||||
export const dbMiddleware: MiddlewareHandler<AppEnv> = async (c, next) => {
|
||||
c.set('db', db);
|
||||
await next();
|
||||
};
|
15
backend/src/types/hono.ts
Normal file
15
backend/src/types/hono.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from '../db/schema';
|
||||
|
||||
// Define the application environment with custom variables
|
||||
export type AppEnv = {
|
||||
Variables: {
|
||||
db: NodePgDatabase<typeof schema>;
|
||||
userId?: string; // For authenticated user
|
||||
};
|
||||
};
|
||||
|
||||
// Export to be used in middleware and route handlers
|
||||
declare module 'hono' {
|
||||
interface ContextVariableMap extends AppEnv['Variables'] {}
|
||||
}
|
74
docker-compose.yml
Normal file
74
docker-compose.yml
Normal file
@ -0,0 +1,74 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: splitwise-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: splitwise
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Database migration service
|
||||
migration:
|
||||
build:
|
||||
context: ./backend
|
||||
target: migration
|
||||
container_name: splitwise-migration
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/splitwise
|
||||
profiles:
|
||||
- migration
|
||||
|
||||
# Drizzle Studio for database management
|
||||
drizzle-studio:
|
||||
build:
|
||||
context: ./backend
|
||||
target: development
|
||||
container_name: splitwise-drizzle-studio
|
||||
command: bun run db:studio
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/splitwise
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- studio
|
||||
|
||||
# Backend service
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
target: development
|
||||
container_name: splitwise-backend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/splitwise
|
||||
PORT: 3000
|
||||
NODE_ENV: development
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
Loading…
Reference in New Issue
Block a user