diff --git a/.gitignore b/.gitignore
index 918f651..cda64ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,5 +43,5 @@ next-env.d.ts
# database
/prisma/*.db*
-src/generated/prisma
+src/generated/*
data
diff --git a/Dockerfile b/Dockerfile
index ffd911a..b60e118 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,6 +16,8 @@ RUN corepack enable
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn prisma:generate
+RUN yarn swagger:generate
+RUN yarn orval:generate
RUN yarn build
# ----- Runner -----
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 3aa4174..feae683 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -13,6 +13,7 @@ services:
- .env.local
volumes:
- ./data:/data
+ - ./src/generated:/app/src/generated
develop:
watch:
- action: sync
@@ -20,8 +21,12 @@ services:
target: /app/src
ignore:
- node_modules/
+ - generated/
- action: rebuild
path: package.json
- action: sync+restart
path: prisma
target: /app/prisma
+ - action: sync+restart
+ path: ./src/app/api
+ target: /app/src/app/api
diff --git a/entrypoint.dev.sh b/entrypoint.dev.sh
index ec103bf..64b45df 100644
--- a/entrypoint.dev.sh
+++ b/entrypoint.dev.sh
@@ -7,4 +7,7 @@ if [ -d "prisma" ]; then
yarn prisma:db:push
fi
+yarn swagger:generate
+yarn orval:generate
+
exec yarn dev
diff --git a/exportSwagger.ts b/exportSwagger.ts
new file mode 100644
index 0000000..1eb2837
--- /dev/null
+++ b/exportSwagger.ts
@@ -0,0 +1,62 @@
+import { registry } from '@/lib/swagger';
+import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
+import fs from 'fs';
+import path from 'path';
+
+function recursiveFileSearch(dir: string, fileList: string[] = []): string[] {
+ const files = fs.readdirSync(dir);
+ files.forEach((file) => {
+ const filePath = path.join(dir, file);
+ if (fs.statSync(filePath).isDirectory()) {
+ recursiveFileSearch(filePath, fileList);
+ } else if (file.match(/swagger\.ts$/)) {
+ fileList.push(filePath);
+ }
+ });
+ return fileList;
+}
+
+async function exportSwagger() {
+ const filesToImport = recursiveFileSearch(
+ path.join(process.cwd(), 'src', 'app', 'api'),
+ );
+
+ await Promise.all(
+ filesToImport.map((file) => {
+ return import(file)
+ .then((module) => {
+ if (module.default) {
+ module.default(registry);
+ }
+ })
+ .catch((error) => {
+ console.error(`Error importing ${file}:`, error);
+ });
+ }),
+ );
+
+ await import('./src/app/api/validation');
+
+ const generator = new OpenApiGeneratorV3(registry.definitions);
+ const spec = generator.generateDocument({
+ openapi: '3.0.0',
+ info: {
+ version: '1.0.0',
+ title: 'MeetUP',
+ description: 'API documentation for MeetUP application',
+ },
+ });
+
+ const outputPath = path.join(
+ process.cwd(),
+ 'src',
+ 'generated',
+ 'swagger.json',
+ );
+ fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2), 'utf8');
+ console.log(`Swagger JSON generated at ${outputPath}`);
+}
+
+exportSwagger().catch((error) => {
+ console.error('Error exporting Swagger:', error);
+});
diff --git a/orval.config.js b/orval.config.js
new file mode 100644
index 0000000..718a86a
--- /dev/null
+++ b/orval.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ meetup: {
+ input: './src/generated/swagger.json',
+ output: {
+ mode: 'tags-split',
+ target: './src/generated/api/meetup.ts',
+ client: 'react-query',
+ },
+ },
+};
diff --git a/package.json b/package.json
index 47fb175..12d6e4e 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,12 @@
"prisma:studio": "dotenv -e .env.local -- prisma studio",
"prisma:db:push": "dotenv -e .env.local -- prisma db push",
"prisma:migrate:reset": "dotenv -e .env.local -- prisma migrate reset",
- "dev_container": "docker compose -f docker-compose.dev.yml up --watch --build"
+ "dev_container": "docker compose -f docker-compose.dev.yml up --watch --build",
+ "swagger:generate": "ts-node -r tsconfig-paths/register exportSwagger.ts",
+ "orval:generate": "orval"
},
"dependencies": {
+ "@asteasolutions/zod-to-openapi": "^8.0.0-beta.4",
"@auth/prisma-adapter": "^2.9.1",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
@@ -33,16 +36,18 @@
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",
+ "@tanstack/react-query": "^5.80.7",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.511.0",
- "next": "15.3.4",
+ "next": "15.4.0-canary.85",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.4",
+ "swagger-ui-react": "^5.24.1",
"tailwind-merge": "^3.2.0",
"zod": "^3.25.60"
},
@@ -52,14 +57,19 @@
"@types/node": "22.15.32",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
+ "@types/swagger-ui-react": "5",
+ "@types/webpack-env": "1.18.8",
"dotenv-cli": "8.0.0",
"eslint": "9.29.0",
"eslint-config-next": "15.3.4",
"eslint-config-prettier": "10.1.5",
+ "orval": "7.10.0",
"postcss": "8.5.6",
"prettier": "3.5.3",
"prisma": "6.9.0",
"tailwindcss": "4.1.10",
+ "ts-node": "10.9.2",
+ "tsconfig-paths": "4.2.0",
"tw-animate-css": "1.3.4",
"typescript": "5.8.3"
},
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 712a068..ffa1c86 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -158,8 +158,8 @@ model Friendship {
requested_at DateTime @default(now())
accepted_at DateTime?
- user1 User @relation("FriendshipUser1", fields: [user_id_1], references: [id])
- user2 User @relation("FriendshipUser2", fields: [user_id_2], references: [id])
+ user1 User @relation("FriendshipUser1", fields: [user_id_1], references: [id], onDelete: Cascade)
+ user2 User @relation("FriendshipUser2", fields: [user_id_2], references: [id], onDelete: Cascade)
@@id([user_id_1, user_id_2])
@@index([user_id_2, status], name: "idx_friendships_user2_status")
@@ -187,8 +187,8 @@ model GroupMember {
role group_member_role @default(MEMBER)
added_at DateTime @default(now())
- group Group @relation(fields: [group_id], references: [id])
- user User @relation(fields: [user_id], references: [id])
+ group Group @relation(fields: [group_id], references: [id], onDelete: Cascade)
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@id([group_id, user_id])
@@index([user_id])
@@ -207,7 +207,7 @@ model BlockedSlot {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
- user User @relation(fields: [user_id], references: [id])
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id, start_time, end_time])
@@index([user_id, is_recurring])
@@ -241,8 +241,8 @@ model MeetingParticipant {
status participant_status @default(PENDING)
added_at DateTime @default(now())
- meeting Meeting @relation(fields: [meeting_id], references: [id])
- user User @relation(fields: [user_id], references: [id])
+ meeting Meeting @relation(fields: [meeting_id], references: [id], onDelete: Cascade)
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@id([meeting_id, user_id])
@@index([user_id, status], name: "idx_participants_user_status")
@@ -259,7 +259,7 @@ model Notification {
is_read Boolean @default(false)
created_at DateTime @default(now())
- user User @relation(fields: [user_id], references: [id])
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id, is_read, created_at], name: "idx_notifications_user_read_time")
@@map("notifications")
@@ -271,7 +271,7 @@ model UserNotificationPreference {
email_enabled Boolean @default(false)
updated_at DateTime @default(now())
- user User @relation(fields: [user_id], references: [id])
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@id([user_id, notification_type])
@@map("user_notification_preferences")
@@ -292,7 +292,7 @@ model EmailQueue {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
- user User @relation(fields: [user_id], references: [id])
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([status, scheduled_at], name: "idx_email_queue_pending_jobs")
@@index([user_id, created_at], name: "idx_email_queue_user_history")
@@ -308,7 +308,7 @@ model CalendarExportToken {
created_at DateTime @default(now())
last_accessed_at DateTime?
- user User @relation(fields: [user_id], references: [id])
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id])
@@map("calendar_export_tokens")
@@ -327,7 +327,7 @@ model CalendarSubscription {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
- user User @relation(fields: [user_id], references: [id])
+ user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
externalEvents ExternalEvent[]
@@index([user_id, is_enabled])
@@ -350,7 +350,7 @@ model ExternalEvent {
show_as_free Boolean @default(false)
last_fetched_at DateTime @default(now())
- subscription CalendarSubscription @relation(fields: [subscription_id], references: [id])
+ subscription CalendarSubscription @relation(fields: [subscription_id], references: [id], onDelete: Cascade)
@@unique([subscription_id, ical_uid], name: "uq_external_event_sub_uid")
@@index([subscription_id, start_time, end_time])
diff --git a/src/app/api-doc/page.tsx b/src/app/api-doc/page.tsx
new file mode 100644
index 0000000..c6e9694
--- /dev/null
+++ b/src/app/api-doc/page.tsx
@@ -0,0 +1,11 @@
+import { getApiDocs } from '@/lib/swagger';
+import ReactSwagger from './react-swagger';
+
+export default async function IndexPage() {
+ const spec = await getApiDocs();
+ return (
+
+ );
+}
diff --git a/src/app/api-doc/react-swagger.tsx b/src/app/api-doc/react-swagger.tsx
new file mode 100644
index 0000000..0dd3931
--- /dev/null
+++ b/src/app/api-doc/react-swagger.tsx
@@ -0,0 +1,14 @@
+'use client';
+
+import SwaggerUI from 'swagger-ui-react';
+import 'swagger-ui-react/swagger-ui.css';
+
+type Props = {
+ spec: object;
+};
+
+function ReactSwagger({ spec }: Props) {
+ return ;
+}
+
+export default ReactSwagger;
diff --git a/src/app/globals.css b/src/app/globals.css
index ec97843..f85cb2f 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -364,3 +364,8 @@
@apply bg-background text-foreground;
}
}
+
+/* Fix for swagger ui readability */
+body:has(.swagger-ui) {
+ @apply bg-white text-black;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 55cdd2c..201a730 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,6 +2,7 @@ import { ThemeProvider } from '@/components/wrappers/theme-provider';
import type { Metadata } from 'next';
import './globals.css';
+import { QueryProvider } from '@/components/query-provider';
export const metadata: Metadata = {
title: 'MeetUp',
@@ -55,7 +56,7 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
- {children}
+ {children}