Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: CI

on:
pull_request:

jobs:
backend:
name: Backend
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: macsync_db
ports:
- 5434:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: src/backend/package-lock.json

- name: Install backend dependencies
working-directory: src/backend
run: npm ci

- name: Create backend .env
working-directory: src/backend
run: |
echo "DATABASE_URL=postgresql://postgres:postgres@localhost:5434/macsync_db" > .env
echo "PORT=3000" >> .env
echo "NODE_ENV=test" >> .env

- name: Run database migrations
working-directory: src/backend
run: npm run db:push
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5434/macsync_db

- name: Lint backend
working-directory: src/backend
run: npm run lint

- name: Build backend
working-directory: src/backend
run: npm run build

web-admin:
name: Web Admin
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: src/frontend/web-admin/package-lock.json

- name: Install web-admin dependencies
working-directory: src/frontend/web-admin
run: npm ci

- name: Lint web-admin
working-directory: src/frontend/web-admin
run: npm run lint

- name: Build web-admin
working-directory: src/frontend/web-admin
run: npm run build
env:
SKIP_ENV_VALIDATION: "1"

root-tests:
name: Root tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Run root test/ folder
run: node test/dummy.test.js

mobile:
name: Mobile (Expo)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: src/frontend/mobile/package-lock.json

- name: Install mobile dependencies
working-directory: src/frontend/mobile
run: npm ci

- name: Type-check mobile app
working-directory: src/frontend/mobile
run: npx tsc --noEmit

- name: Validate Expo config
working-directory: src/frontend/mobile
run: npx expo config --type prebuild 2>/dev/null || true
29 changes: 18 additions & 11 deletions src/backend/src/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,23 @@ export class AppService {
*/
async getAllTables() {
const db = this.dbService.db;
const [usersRows, rolesRows, userRolesRows, eventsRows, ticketsRows, tableSeatsRows, busSeatsRows] =
await Promise.all([
db.select().from(users),
db.select().from(roles),
db.select().from(userRoles),
db.select().from(events),
db.select().from(tickets),
db.select().from(tableSeats),
db.select().from(busSeats),
]);
const [
usersRows,
rolesRows,
userRolesRows,
eventsRows,
ticketsRows,
tableSeatsRows,
busSeatsRows,
] = await Promise.all([
db.select().from(users),
db.select().from(roles),
db.select().from(userRoles),
db.select().from(events),
db.select().from(tickets),
db.select().from(tableSeats),
db.select().from(busSeats),
]);
return {
users: usersRows,
roles: rolesRows,
Expand All @@ -95,7 +102,7 @@ export class AppService {
try {
await this.dbService.db.execute('SELECT 1');
dbStatus = 'connected';
} catch (error) {
} catch {
dbStatus = 'error';
}

Expand Down
4 changes: 3 additions & 1 deletion src/backend/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ export type NewBusSeat = typeof busSeats.$inferInsert;
// ------------------- SEAT RESERVATIONS (hold seat during Stripe checkout; expire after 5 min or on payment/cancel) -------------------
export const seatReservations = pgTable('seat_reservations', {
id: serial('id').primaryKey(),
stripeSessionId: varchar('stripe_session_id', { length: 255 }).notNull().unique(),
stripeSessionId: varchar('stripe_session_id', { length: 255 })
.notNull()
.unique(),
eventId: integer('event_id')
.notNull()
.references(() => events.id, { onDelete: 'cascade' }),
Expand Down
9 changes: 5 additions & 4 deletions src/backend/src/db/seed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export async function runSeedDb(db: SeedDb): Promise<boolean> {
if (!user1 || !user2 || !user3) throw new Error('User insert failed');

await db.insert(userRoles).values([
{ userId: user1.id, roleId: roleAdmin!.id },
{ userId: user2.id, roleId: roleMember!.id },
{ userId: user3.id, roleId: roleGuest!.id },
{ userId: user1.id, roleId: roleAdmin.id },
{ userId: user2.id, roleId: roleMember.id },
{ userId: user3.id, roleId: roleGuest.id },
]);

const [event1, event2] = await db
Expand Down Expand Up @@ -114,7 +114,8 @@ export async function runSeedDb(db: SeedDb): Promise<boolean> {
}
}
}
if (tableSeatRows.length > 0) await db.insert(tableSeats).values(tableSeatRows);
if (tableSeatRows.length > 0)
await db.insert(tableSeats).values(tableSeatRows);

const busSeatRows: Array<{
eventId: number;
Expand Down
19 changes: 14 additions & 5 deletions src/backend/src/db/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,27 @@ async function main() {
console.log(' ⏭️ Database already has data. Skipping.');
} else {
console.log(' ✓ Roles, users, user_roles');
console.log(' ✓ Events with table_count, seats_per_table, bus_count, bus_capacity');
console.log(
' ✓ Events with table_count, seats_per_table, bus_count, bus_capacity',
);
console.log(' ✓ Table seats and bus seats auto-generated');
console.log(' ✓ Sample tickets and seat assignments');
console.log('✅ Seed completed.');
}
await pool.end();
}

main().catch((err) => {
const cause = err?.cause ?? err;
const msg = String(cause?.message ?? err?.message ?? err);
const code = cause?.code ?? err?.code;
main().catch((err: unknown) => {
const msg =
err instanceof Error
? err.message
: typeof err === 'object' && err !== null && 'message' in err
? String((err as { message: unknown }).message)
: String(err);
const code =
typeof err === 'object' && err !== null && 'code' in err
? (err as { code: string }).code
: undefined;
if (code === '42P01' || /relation .* does not exist/i.test(msg)) {
console.error('❌ Seed failed: Database tables do not exist.');
console.error('');
Expand Down
5 changes: 1 addition & 4 deletions src/backend/src/events/events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ export class EventsController {
}

@Get(':id')
findOne(
@Param('id') id: string,
@Query('userId') userId?: string,
) {
findOne(@Param('id') id: string, @Query('userId') userId?: string) {
const uid = userId != null && userId !== '' ? +userId : undefined;
return this.eventsService.findOne(+id, uid);
}
Expand Down
40 changes: 14 additions & 26 deletions src/backend/src/events/events.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../db/schema';
import type { NewEvent } from '../db/schema';
import { eq, sql, and } from 'drizzle-orm';
import type { SeedDb } from '../db/seed-data';
import {
createTableSeatsForEvent,
createBusSeatsForEvent,
Expand All @@ -22,7 +23,6 @@ export class EventsService {
private readonly paymentsService: PaymentsService,
) {}


async findAll() {
const rows = await this.dbService.db
.select({
Expand Down Expand Up @@ -76,24 +76,21 @@ export class EventsService {
.from(events)
.where(eq(events.id, id));

let row = rows[0] ?? null;
const row = rows[0] ?? null;
if (!row) return null;

let userTicket: { tableSeat: string | null; busSeat: string | null } | null =
null;
let userTicket: {
tableSeat: string | null;
busSeat: string | null;
} | null = null;
if (userId != null) {
const ticketRows = await this.dbService.db
.select({
tableSeat: tickets.tableSeat,
busSeat: tickets.busSeat,
})
.from(tickets)
.where(
and(
eq(tickets.eventId, id),
eq(tickets.userId, userId),
),
)
.where(and(eq(tickets.eventId, id), eq(tickets.userId, userId)))
.limit(1);
if (ticketRows[0]) {
userTicket = {
Expand All @@ -106,12 +103,11 @@ export class EventsService {
return { ...row, userTicket };
}


async create(event: NewEvent) {
// Ensure date is a Date object (JSON sends it as a string)
const values = {
...event,
date: new Date(event.date as any),
date: new Date(event.date as string | Date),
};

const result = await this.dbService.db
Expand All @@ -130,21 +126,17 @@ export class EventsService {
created.seatsPerTable
) {
await createTableSeatsForEvent(
this.dbService.db as any,
this.dbService.db as SeedDb,
created.id,
created.tableCount,
created.seatsPerTable,
);
}

// Auto-create bus seats if needed
if (
created.requiresBusSignup &&
created.busCount &&
created.busCapacity
) {
if (created.requiresBusSignup && created.busCount && created.busCapacity) {
await createBusSeatsForEvent(
this.dbService.db as any,
this.dbService.db as SeedDb,
created.id,
created.busCount,
created.busCapacity,
Expand Down Expand Up @@ -201,9 +193,7 @@ export class EventsService {
.where(eq(tableSeats.ticketId, ticket.id));

// Delete ticket
await this.dbService.db
.delete(tickets)
.where(eq(tickets.id, ticket.id));
await this.dbService.db.delete(tickets).where(eq(tickets.id, ticket.id));

return { cancelled: true };
}
Expand Down Expand Up @@ -286,8 +276,7 @@ export class EventsService {

const reservationMinutes = 5;
const expiresAt = new Date(now.getTime() + reservationMinutes * 60 * 1000);
const notReservedTable =
sql`${tableSeats.id} NOT IN (SELECT table_seat_id FROM seat_reservations WHERE table_seat_id IS NOT NULL AND expires_at > ${now})`;
const notReservedTable = sql`${tableSeats.id} NOT IN (SELECT table_seat_id FROM seat_reservations WHERE table_seat_id IS NOT NULL AND expires_at > ${now})`;

let reservedTableSeatId: number | null = null;
if (ev.requiresTableSignup) {
Expand Down Expand Up @@ -320,8 +309,7 @@ export class EventsService {

let reservedBusSeatId: number | null = null;
if (ev.requiresBusSignup) {
const notReservedBus =
sql`${busSeats.id} NOT IN (SELECT bus_seat_id FROM seat_reservations WHERE bus_seat_id IS NOT NULL AND expires_at > ${now})`;
const notReservedBus = sql`${busSeats.id} NOT IN (SELECT bus_seat_id FROM seat_reservations WHERE bus_seat_id IS NOT NULL AND expires_at > ${now})`;
const busAvailable = await this.dbService.db
.select({ id: busSeats.id })
.from(busSeats)
Expand Down
Loading