lapeh 2.2.3 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -6,15 +6,60 @@ const { execSync } = require('child_process');
6
6
  const readline = require('readline');
7
7
 
8
8
  const args = process.argv.slice(2);
9
+ const command = args[0];
10
+
11
+ // Register tsconfig paths for development
12
+ // require('tsconfig-paths/register');
13
+
14
+ switch (command) {
15
+ case 'dev':
16
+ runDev();
17
+ break;
18
+ case 'start':
19
+ runStart();
20
+ break;
21
+ case 'build':
22
+ runBuild();
23
+ break;
24
+ case 'upgrade':
25
+ (async () => {
26
+ await upgradeProject();
27
+ })();
28
+ break;
29
+ default:
30
+ createProject();
31
+ break;
32
+ }
9
33
 
10
- // --- UPGRADE MODE ---
11
- if (args[0] === 'upgrade') {
12
- (async () => {
13
- await upgradeProject();
14
- })();
15
- } else {
16
- // --- CREATE MODE ---
17
- createProject();
34
+ function runDev() {
35
+ console.log('🚀 Starting Lapeh in development mode...');
36
+ try {
37
+ const tsNodePath = require.resolve('ts-node/register');
38
+ const tsConfigPathsPath = require.resolve('tsconfig-paths/register');
39
+ // We execute a script that requires ts-node to run lib/bootstrap.ts
40
+ // In a real package, this would be `node dist/lib/bootstrap.js`
41
+ execSync(`npx nodemon --exec "node -r ${tsNodePath} -r ${tsConfigPathsPath}" -e "require('./lib/bootstrap').bootstrap()"`, { stdio: 'inherit' });
42
+ } catch (error) {
43
+ // Ignore error
44
+ }
45
+ }
46
+
47
+ function runStart() {
48
+ console.log('🚀 Starting Lapeh production server...');
49
+ const distPath = path.join(process.cwd(), 'dist/lib/bootstrap.js');
50
+ // For production, we assume built files
51
+ const cmd = `node -e "require('${distPath.replace(/\\/g, '/')}').bootstrap()"`;
52
+ execSync(cmd, {
53
+ stdio: 'inherit',
54
+ env: { ...process.env, NODE_ENV: 'production' }
55
+ });
56
+ }
57
+
58
+ function runBuild() {
59
+ console.log('🛠️ Building Lapeh project...');
60
+ execSync('npm run prisma:generate', { stdio: 'inherit' });
61
+ execSync('npx tsc && npx tsc-alias', { stdio: 'inherit' });
62
+ console.log('✅ Build complete.');
18
63
  }
19
64
 
20
65
  async function upgradeProject() {
@@ -0,0 +1,73 @@
1
+ # Panduan Arsitektur: Menuju "Framework as a Dependency" (Next.js Style)
2
+
3
+ Saat ini, Lapeh menggunakan pendekatan **Boilerplate** (seperti Laravel), di mana pengguna mendapatkan seluruh kode sumber (`src/`) dan bertanggung jawab atas `express`, `prisma`, dll.
4
+
5
+ Untuk mengubahnya menjadi seperti **Next.js** (di mana pengguna hanya menginstall `lapeh` dan `package.json` mereka bersih), kita perlu mengubah arsitektur menjadi **Library**.
6
+
7
+ ## 1. Perbedaan Utama
8
+
9
+ | Fitur | Boilerplate (Lapeh Saat Ini) | Library (Next.js Style) |
10
+ | :--- | :--- | :--- |
11
+ | **Instalasi** | `git clone` / `npx create-lapeh` | `npm install lapeh` |
12
+ | **package.json** | Banyak dependency (`express`, `cors`, dll) | Sedikit (`lapeh`, `react`) |
13
+ | **Scripts** | Panjang (`nodemon src/index.ts`) | Pendek (`lapeh dev`) |
14
+ | **Core Code** | Terbuka di `src/core/` | Tersembunyi di `node_modules/lapeh` |
15
+ | **Update** | Susah (harus merge manual) | Mudah (`npm update lapeh`) |
16
+
17
+ ## 2. Langkah Implementasi
18
+
19
+ Saya telah memulai langkah pertama dengan menambahkan **CLI Runner** di `bin/index.js`.
20
+
21
+ ### A. Update CLI (`bin/index.js`) ✅ (Sudah Dilakukan)
22
+ Saya sudah menambahkan command `dev`, `start`, dan `build` ke dalam CLI Lapeh. Ini memungkinkan pengguna menjalankan server tanpa tahu perintah aslinya.
23
+
24
+ ```javascript
25
+ // Contoh penggunaan nanti:
26
+ "scripts": {
27
+ "dev": "lapeh dev",
28
+ "build": "lapeh build",
29
+ "start": "lapeh start"
30
+ }
31
+ ```
32
+
33
+ ### B. Struktur Project Pengguna (Target)
34
+ Nantinya, project pengguna Lapeh hanya akan berisi file bisnis mereka:
35
+
36
+ ```text
37
+ my-app/
38
+ ├── src/
39
+ │ ├── controllers/
40
+ │ ├── routes/
41
+ │ └── models/
42
+ ├── lapeh.config.ts <-- Konfigurasi framework (pengganti edit core)
43
+ └── package.json
44
+ ```
45
+
46
+ Dan `package.json` mereka akan terlihat seperti ini:
47
+
48
+ ```json
49
+ {
50
+ "name": "my-app",
51
+ "dependencies": {
52
+ "lapeh": "^2.0.0"
53
+ },
54
+ "scripts": {
55
+ "dev": "lapeh dev",
56
+ "build": "lapeh build",
57
+ "start": "lapeh start"
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### C. Apa yang Harus Dilakukan Selanjutnya?
63
+
64
+ 1. **Publish Package**: Anda perlu mempublish folder framework ini ke NPM (atau private registry).
65
+ * Pastikan `express`, `cors`, `helmet`, dll ada di `dependencies` (bukan `devDependencies`).
66
+ 2. **Abstraksi `src/index.ts`**:
67
+ * Saat ini `src/index.ts` adalah entry point yang diedit user.
68
+ * Ubah agar `lapeh dev` menjalankan server internal yang **mengimpor** routes/controller user secara dinamis (seperti Next.js pages router).
69
+ 3. **Config Loader**:
70
+ * Buat sistem pembacaan `lapeh.config.ts` untuk mengatur Port, Database URL, dll tanpa mengedit kode core.
71
+
72
+ ## 3. Kesimpulan
73
+ Perubahan yang saya lakukan di `bin/index.js` adalah fondasi untuk CLI style. Untuk mencapai "Clean package.json" sepenuhnya, Anda harus memisahkan **Framework Core** (repo ini) dengan **User Project** (repo baru yang menginstall framework ini).
package/doc/CHEATSHEET.md CHANGED
@@ -4,48 +4,50 @@ Referensi cepat untuk perintah dan kode yang sering digunakan.
4
4
 
5
5
  ## 💻 CLI Commands
6
6
 
7
- | Perintah | Fungsi |
8
- | :--- | :--- |
9
- | **`npm run dev`** | Menjalankan server development (hot-reload). |
10
- | **`npm run typecheck`** | Cek error TypeScript (tanpa compile). |
11
- | **`npm run lint`** | Cek kode kotor/variabel tidak terpakai. |
12
- | **`npm run lint:fix`** | Perbaiki kode kotor otomatis. |
13
- | **`npm run make:module <Name>`** | Buat Controller, Route, & Model sekaligus. |
14
- | **`npm run make:controller <Name>`** | Buat Controller saja. |
15
- | **`npm run make:model <Name>`** | Buat Model Prisma saja. |
16
- | **`npm run prisma:migrate`** | Apply perubahan schema ke DB lokal. |
17
- | **`npm run db:studio`** | Buka GUI Database. |
18
- | **`npm run db:seed`** | Isi data dummy. |
19
- | **`npm run db:reset`** | Hapus DB & mulai dari nol. |
7
+ | Perintah | Fungsi |
8
+ | :----------------------------------- | :------------------------------------------- |
9
+ | **`npm run dev`** | Menjalankan server development (hot-reload). |
10
+ | **`npm run typecheck`** | Cek error TypeScript (tanpa compile). |
11
+ | **`npm run lint`** | Cek kode kotor/variabel tidak terpakai. |
12
+ | **`npm run lint:fix`** | Perbaiki kode kotor otomatis. |
13
+ | **`npm run make:module <Name>`** | Buat Controller, Route, & Model sekaligus. |
14
+ | **`npm run make:controller <Name>`** | Buat Controller saja. |
15
+ | **`npm run make:model <Name>`** | Buat Model Prisma saja. |
16
+ | **`npm run prisma:migrate`** | Apply perubahan schema ke DB lokal. |
17
+ | **`npm run db:studio`** | Buka GUI Database. |
18
+ | **`npm run db:seed`** | Isi data dummy. |
19
+ | **`npm run db:reset`** | Hapus DB & mulai dari nol. |
20
20
 
21
21
  ## 🛡️ Validator Rules (Laravel-Style)
22
22
 
23
23
  Gunakan di `Validator.make(data, rules)`.
24
24
 
25
- | Rule | Deskripsi | Contoh |
26
- | :--- | :--- | :--- |
27
- | `required` | Wajib ada & tidak null. | `"required"` |
28
- | `string` | Harus text. | `"required|string"` |
29
- | `number` | Harus angka. | `"required|number"` |
30
- | `email` | Format email valid. | `"required|email"` |
31
- | `min:X` | Min panjang/nilai. | `"min:8"` (pass), `"min:18"` (umur) |
32
- | `max:X` | Max panjang/nilai. | `"max:255"` |
33
- | `unique:table,col` | Cek unik di DB. | `"unique:users,email"` |
34
- | `exists:table,col` | Cek exist di DB. | `"exists:roles,id"` |
35
- | `image` | File harus gambar. | `"required|image"` |
36
- | `mimes:types` | File extension. | `"mimes:pdf,docx"` |
25
+ | Rule | Deskripsi | Contoh |
26
+ | :----------------- | :---------------------- | :---------------------------------- | -------- |
27
+ | `required` | Wajib ada & tidak null. | `"required"` |
28
+ | `string` | Harus text. | `"required | string"` |
29
+ | `number` | Harus angka. | `"required | number"` |
30
+ | `email` | Format email valid. | `"required | email"` |
31
+ | `min:X` | Min panjang/nilai. | `"min:8"` (pass), `"min:18"` (umur) |
32
+ | `max:X` | Max panjang/nilai. | `"max:255"` |
33
+ | `unique:table,col` | Cek unik di DB. | `"unique:users,email"` |
34
+ | `exists:table,col` | Cek exist di DB. | `"exists:roles,id"` |
35
+ | `image` | File harus gambar. | `"required | image"` |
36
+ | `mimes:types` | File extension. | `"mimes:pdf,docx"` |
37
37
 
38
38
  ## 🔑 Authentication
39
39
 
40
40
  **Middleware di Route:**
41
+
41
42
  ```typescript
42
43
  import { requireAuth, requireAdmin } from "@/middleware/auth";
43
44
 
44
- router.get("/profile", requireAuth, getProfile); // Login User
45
+ router.get("/profile", requireAuth, getProfile); // Login User
45
46
  router.delete("/user", requireAuth, requireAdmin, del); // Admin Only
46
47
  ```
47
48
 
48
49
  **Akses User di Controller:**
50
+
49
51
  ```typescript
50
52
  // (req as any).user tersedia setelah requireAuth
51
53
  const userId = (req as any).user.userId;
@@ -55,22 +57,25 @@ const role = (req as any).user.role;
55
57
  ## ⚡ Fast Response (Serializer)
56
58
 
57
59
  **1. Schema:**
60
+
58
61
  ```typescript
59
62
  const schema = {
60
63
  type: "object",
61
64
  properties: {
62
65
  id: { type: "string" },
63
- name: { type: "string" }
64
- }
66
+ name: { type: "string" },
67
+ },
65
68
  };
66
69
  ```
67
70
 
68
71
  **2. Serializer:**
72
+
69
73
  ```typescript
70
74
  const serializer = getSerializer("key-name", createResponseSchema(schema));
71
75
  ```
72
76
 
73
77
  **3. Send:**
78
+
74
79
  ```typescript
75
80
  sendFastSuccess(res, 200, serializer, { ...data });
76
81
  ```
@@ -78,7 +83,7 @@ sendFastSuccess(res, 200, serializer, { ...data });
78
83
  ## 📦 Redis (Cache)
79
84
 
80
85
  ```typescript
81
- import { redis } from "@/core/redis";
86
+ import { redis } from "@lapeh/core/redis";
82
87
 
83
88
  // Set Cache (Key, Value, Mode, Detik)
84
89
  await redis.set("profile:1", JSON.stringify(data), "EX", 3600);
package/doc/FEATURES.md CHANGED
@@ -6,12 +6,12 @@ Dokumen ini menjelaskan fitur-fitur utama Lapeh Framework dan cara penggunaannya
6
6
 
7
7
  Framework ini menyediakan utility `Validator` yang terinspirasi dari Laravel, menggunakan `zod` di belakang layar namun dengan API yang lebih string-based dan mudah dibaca.
8
8
 
9
- **Lokasi:** `@/utils/validator`
9
+ **Lokasi:** `@lapeh/utils/validator`
10
10
 
11
11
  ### Penggunaan Dasar
12
12
 
13
13
  ```typescript
14
- import { Validator } from "@/utils/validator";
14
+ import { Validator } from "@lapeh/utils/validator";
15
15
 
16
16
  export async function createProduct(req: Request, res: Response) {
17
17
  const validator = await Validator.make(req.body, {
@@ -79,7 +79,7 @@ Untuk endpoint yang membutuhkan performa tinggi (misalnya list data besar), guna
79
79
  3. **Kirim Response**
80
80
 
81
81
  ```typescript
82
- import { sendFastSuccess } from "@/utils/response";
82
+ import { sendFastSuccess } from "@lapeh/utils/response";
83
83
 
84
84
  // Di dalam controller
85
85
  sendFastSuccess(res, 200, productSerializer, {
package/doc/STRUCTURE.md CHANGED
@@ -4,56 +4,73 @@ Untuk memahami Lapeh Framework sepenuhnya, Anda perlu tahu apa fungsi setiap fil
4
4
 
5
5
  ## Root Directory
6
6
 
7
- | File/Folder | Deskripsi |
8
- | :--- | :--- |
9
- | `bin/` | Berisi script eksekusi untuk CLI (`npx lapeh`). Anda jarang menyentuh ini. |
10
- | `doc/` | Dokumentasi proyek ini berada. |
11
- | `prisma/` | Jantung konfigurasi Database. |
12
- | `scripts/` | Kumpulan script Node.js untuk utility (generator, compiler schema, dll). |
13
- | `src/` | **Source Code Utama**. 99% kodingan Anda ada di sini. |
14
- | `.env` | Variabel rahasia (Database URL, API Keys). **Jangan commit file ini ke Git!** |
15
- | `docker-compose.yml` | Konfigurasi Docker untuk menjalankan Database & Redis lokal. |
16
- | `nodemon.json` | Konfigurasi auto-restart saat development. |
17
- | `package.json` | Daftar library (dependencies) dan perintah (`npm run ...`). |
18
- | `tsconfig.json` | Konfigurasi TypeScript. |
19
-
20
- ## Folder `src/` (Source Code)
7
+ | File/Folder | Deskripsi |
8
+ | :------------------- | :---------------------------------------------------------------------------- |
9
+ | `bin/` | Berisi script eksekusi untuk CLI (`npx lapeh`). Anda jarang menyentuh ini. |
10
+ | `doc/` | Dokumentasi proyek ini berada. |
11
+ | `lib/` | **Framework Core**. Bagian internal framework yang jarang Anda sentuh. |
12
+ | `prisma/` | Jantung konfigurasi Database. |
13
+ | `scripts/` | Kumpulan script Node.js untuk utility (generator, compiler schema, dll). |
14
+ | `src/` | **Source Code Utama**. 99% kodingan Anda ada di sini. |
15
+ | `.env` | Variabel rahasia (Database URL, API Keys). **Jangan commit file ini ke Git!** |
16
+ | `docker-compose.yml` | Konfigurasi Docker untuk menjalankan Database & Redis lokal. |
17
+ | `nodemon.json` | Konfigurasi auto-restart saat development. |
18
+ | `package.json` | Daftar library (dependencies) dan perintah (`npm run ...`). |
19
+ | `tsconfig.json` | Konfigurasi TypeScript. |
20
+
21
+ ## Folder `src/` (Source Code - User Space)
21
22
 
22
23
  Ini adalah tempat Anda bekerja setiap hari.
23
24
 
24
25
  ### `src/controllers/`
26
+
25
27
  Berisi logika aplikasi. Controller menerima Request, memprosesnya, dan mengembalikan Response.
28
+
26
29
  - **Contoh**: `authController.ts` menangani login/register.
27
- - **Tips**: Jangan taruh *business logic* yang terlalu kompleks di sini. Gunakan Service (opsional) jika controller sudah terlalu gemuk.
30
+ - **Tips**: Jangan taruh _business logic_ yang terlalu kompleks di sini. Gunakan Service (opsional) jika controller sudah terlalu gemuk.
28
31
 
29
32
  ### `src/models/`
33
+
30
34
  Berisi definisi tabel database (Schema Prisma).
35
+
31
36
  - **Unik di Lapeh**: Kami memecah `schema.prisma` yang besar menjadi file-file kecil per fitur (misal `user.prisma`, `product.prisma`) agar mudah di-manage. Script `prisma:migrate` akan menggabungkannya nanti.
32
37
 
33
38
  ### `src/routes/`
39
+
34
40
  Mendefinisikan URL endpoint.
41
+
35
42
  - Menghubungkan URL (misal `/api/login`) ke fungsi di Controller.
36
43
  - Menempelkan Middleware (misal `requireAuth`).
37
44
 
38
- ### `src/middleware/`
39
- Kode yang berjalan *sebelum* Controller.
40
- - `auth.ts`: Cek JWT Token.
41
- - `rateLimit.ts`: Batasi jumlah request.
42
- - `requestLogger.ts`: Log setiap request yang masuk.
45
+ ## Folder `lib/` (Framework Internals)
43
46
 
44
- ### `src/utils/`
45
- Fungsi bantuan (Helper) yang bisa dipakai di mana saja.
46
- - `validator.ts`: Validasi input ala Laravel.
47
- - `response.ts`: Standar format JSON response (`sendSuccess`, `sendError`).
48
- - `logger.ts`: Sistem logging (Winston).
47
+ Bagian ini mirip dengan `node_modules` atau folder `.next` di Next.js. Ini adalah mesin framework.
48
+
49
+ ### `lib/core/`
50
+
51
+ Bagian "Mesin" framework.
49
52
 
50
- ### `src/core/`
51
- Bagian "Mesin" framework. Anda jarang perlu mengubah ini kecuali ingin memodifikasi behavior dasar framework.
52
53
  - `server.ts`: Setup Express App.
53
54
  - `database.ts`: Instance Prisma Client.
54
55
  - `redis.ts`: Koneksi Redis.
55
56
  - `serializer.ts`: Logic caching JSON Schema.
56
57
 
58
+ ### `lib/middleware/`
59
+
60
+ Middleware bawaan framework.
61
+
62
+ - `auth.ts`: Cek JWT Token.
63
+ - `rateLimit.ts`: Batasi jumlah request.
64
+ - `requestLogger.ts`: Log setiap request yang masuk.
65
+
66
+ ### `lib/utils/`
67
+
68
+ Fungsi bantuan (Helper) bawaan.
69
+
70
+ - `validator.ts`: Validasi input ala Laravel.
71
+ - `response.ts`: Standar format JSON response (`sendFastSuccess`, `sendError`).
72
+ - `logger.ts`: Sistem logging (Winston).
73
+
57
74
  ## Folder `prisma/`
58
75
 
59
76
  - `migrations/`: History perubahan database (SQL file). Jangan diedit manual.
@@ -63,6 +80,7 @@ Bagian "Mesin" framework. Anda jarang perlu mengubah ini kecuali ingin memodifik
63
80
  ## Folder `scripts/`
64
81
 
65
82
  Script-script "Magic" yang dijalankan `npm run`.
83
+
66
84
  - `make-controller.js`: Generator controller.
67
85
  - `compile-schema.js`: Penggabung file `.prisma`.
68
86
  - `init-project.js`: Wizard setup awal.
@@ -0,0 +1,145 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+
4
+ import express, { Request, Response, NextFunction } from "express";
5
+ import cors from "cors";
6
+ import helmet from "helmet";
7
+ import compression from "compression";
8
+ import http from "http";
9
+ import path from "path";
10
+ import { initRealtime } from "./core/realtime";
11
+ import { initRedis, redis } from "./core/redis";
12
+ import { prisma } from "./core/database";
13
+ import { visitorCounter } from "./middleware/visitor";
14
+ import { errorHandler } from "./middleware/error";
15
+ import { apiLimiter } from "./middleware/rateLimit";
16
+ import { requestLogger } from "./middleware/requestLogger";
17
+ import { sendSuccess } from "./utils/response";
18
+
19
+ export async function bootstrap() {
20
+ // Validasi Environment Variables
21
+ const requiredEnvs = ["DATABASE_URL", "JWT_SECRET"];
22
+ const missingEnvs = requiredEnvs.filter((key) => !process.env[key]);
23
+ if (missingEnvs.length > 0) {
24
+ console.error(
25
+ `❌ Missing required environment variables: ${missingEnvs.join(", ")}`
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ const app = express();
31
+
32
+ app.disable("x-powered-by");
33
+ app.use(compression());
34
+
35
+ // Request Timeout Middleware (30s)
36
+ app.use((_req: Request, res: Response, next: NextFunction) => {
37
+ res.setTimeout(30000, () => {
38
+ res.status(408).send({
39
+ status: "error",
40
+ message: "Request Timeout (30s limit)",
41
+ });
42
+ });
43
+ next();
44
+ });
45
+
46
+ app.use(
47
+ helmet({
48
+ contentSecurityPolicy: false,
49
+ crossOriginResourcePolicy: { policy: "cross-origin" },
50
+ })
51
+ );
52
+
53
+ const corsOrigin = process.env.CORS_ORIGIN || "*";
54
+ app.use(
55
+ cors({
56
+ origin: corsOrigin,
57
+ credentials: true,
58
+ exposedHeaders: ["x-access-token", "x-access-expires-at"],
59
+ })
60
+ );
61
+
62
+ app.use(requestLogger);
63
+ app.use(express.json({ limit: "10mb" }));
64
+ app.use(express.urlencoded({ extended: true, limit: "10mb" }));
65
+ app.use(apiLimiter);
66
+ app.use(visitorCounter);
67
+
68
+ // Health Check
69
+ app.get("/", (_req: Request, res: Response) => {
70
+ sendSuccess(res, 200, "Lapeh API is running", {
71
+ status: "active",
72
+ timestamp: new Date(),
73
+ version: process.env.npm_package_version || "unknown",
74
+ });
75
+ });
76
+
77
+ // DYNAMIC ROUTE LOADING
78
+ try {
79
+ const isProduction = process.env.NODE_ENV === "production";
80
+ const userRoutesPath = isProduction
81
+ ? path.join(process.cwd(), "dist", "src", "routes")
82
+ : path.join(process.cwd(), "src", "routes");
83
+
84
+ // Gunakan require agar sinkron dan mudah dicatch
85
+ const { apiRouter } = require(userRoutesPath);
86
+ app.use("/api", apiRouter);
87
+ console.log(
88
+ `✅ User routes loaded successfully from ${
89
+ isProduction ? "dist/" : ""
90
+ }src/routes`
91
+ );
92
+ } catch (error) {
93
+ console.warn(
94
+ "⚠️ Could not load user routes. Make sure you export 'apiRouter'."
95
+ );
96
+ console.error(error);
97
+ }
98
+
99
+ app.use(errorHandler);
100
+
101
+ const port = process.env.PORT ? Number(process.env.PORT) : 4000;
102
+ const server = http.createServer(app);
103
+
104
+ initRealtime(server);
105
+
106
+ try {
107
+ await initRedis();
108
+
109
+ server.on("error", (e: any) => {
110
+ if (e.code === "EADDRINUSE") {
111
+ console.log(`\n❌ Error: Port ${port} is already in use.`);
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ server.listen(port, () => {
117
+ console.log(`✅ API running at http://localhost:${port}`);
118
+ console.log(`🛡️ Environment: ${process.env.NODE_ENV || "development"}`);
119
+ });
120
+ } catch (error) {
121
+ console.error("❌ Failed to start server:", error);
122
+ process.exit(1);
123
+ }
124
+
125
+ // Graceful Shutdown
126
+ const shutdown = async (signal: string) => {
127
+ console.log(`\n🛑 ${signal} received. Closing resources...`);
128
+ server.close(() => console.log("Http server closed."));
129
+ try {
130
+ await prisma.$disconnect();
131
+ if (redis && redis.status === "ready") await redis.quit();
132
+ process.exit(0);
133
+ } catch (err) {
134
+ console.error("Error during shutdown:", err);
135
+ process.exit(1);
136
+ }
137
+ };
138
+
139
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
140
+ process.on("SIGINT", () => shutdown("SIGINT"));
141
+ process.on("uncaughtException", (error) => {
142
+ console.error("❌ Uncaught Exception:", error);
143
+ shutdown("uncaughtException");
144
+ });
145
+ }
@@ -1,9 +1,10 @@
1
- import express, { Request, Response } from "express";
1
+ import express, { Request, Response, NextFunction } from "express";
2
2
  import cors from "cors";
3
3
  import helmet from "helmet";
4
- import { apiRouter } from "../routes"; // Import unified routes
4
+ import compression from "compression";
5
+ // import { apiRouter } from "@/routes"; // Routes are now loaded dynamically in bootstrap.ts
5
6
  import { visitorCounter } from "../middleware/visitor";
6
- import { errorHandler } from "../middleware/error";
7
+ // import { errorHandler } from "../middleware/error";
7
8
  import { apiLimiter } from "../middleware/rateLimit";
8
9
  import { requestLogger } from "../middleware/requestLogger";
9
10
  import { sendSuccess } from "../utils/response";
@@ -12,6 +13,20 @@ export const app = express();
12
13
 
13
14
  app.disable("x-powered-by");
14
15
 
16
+ // Compression (Gzip)
17
+ app.use(compression());
18
+
19
+ // Request Timeout Middleware (30s)
20
+ app.use((_req: Request, res: Response, next: NextFunction) => {
21
+ res.setTimeout(30000, () => {
22
+ res.status(408).send({
23
+ status: "error",
24
+ message: "Request Timeout (30s limit)",
25
+ });
26
+ });
27
+ next();
28
+ });
29
+
15
30
  // Security Headers
16
31
  app.use(
17
32
  helmet({
@@ -48,8 +63,8 @@ app.get("/", (_req: Request, res: Response) => {
48
63
  });
49
64
  });
50
65
 
51
- // Routes
52
- app.use("/api", apiRouter);
66
+ // Routes are loaded in bootstrap.ts via app.use('/api', userApiRouter)
53
67
 
54
68
  // Global Error Handler
55
- app.use(errorHandler);
69
+ // Note: We don't attach error handler here because we want to attach it AFTER routes are loaded in bootstrap
70
+ // app.use(errorHandler);
@@ -1,7 +1,14 @@
1
1
  import { Request, Response, NextFunction } from "express";
2
2
  import jwt from "jsonwebtoken";
3
3
  import { sendError } from "../utils/response";
4
- import { ACCESS_TOKEN_EXPIRES_IN_SECONDS } from "../controllers/authController";
4
+ // Note: We should ideally avoid importing from controllers in middleware
5
+ // But for now we'll keep it to maintain functionality, but point to src if needed
6
+ // However, authController is in src (user land) or lib?
7
+ // Wait, authController was NOT moved to lib. It is in src/controllers.
8
+ // So this import will fail if we use relative paths.
9
+ // But we are in lib.
10
+ // We should probably move ACCESS_TOKEN_EXPIRES_IN_SECONDS to a config or constants file in lib.
11
+ const ACCESS_TOKEN_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
5
12
 
6
13
  export function requireAuth(req: Request, res: Response, next: NextFunction) {
7
14
  const header = req.headers.authorization;
@@ -35,7 +42,7 @@ export function requireAuth(req: Request, res: Response, next: NextFunction) {
35
42
  res.setHeader("x-access-expires-at", accessExpiresAt);
36
43
 
37
44
  next();
38
- } catch {
45
+ } catch (err: any) {
39
46
  sendError(res, 401, "Invalid token");
40
47
  }
41
48
  }
@@ -27,6 +27,11 @@ export function errorHandler(
27
27
  const fields = target.length > 0 ? target.join(", ") : "field";
28
28
  return sendError(res, 409, `Unique constraint failed on: ${fields}`);
29
29
  }
30
+ // P2003: Foreign key constraint failed
31
+ if (err.code === "P2003") {
32
+ const field = err.meta?.field_name || "unknown field";
33
+ return sendError(res, 400, `Foreign key constraint failed on: ${field}`);
34
+ }
30
35
  // P2025: Record not found
31
36
  if (err.code === "P2025") {
32
37
  return sendError(res, 404, "Record not found");