lapeh 2.2.3 → 2.2.4
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 +51 -8
- package/doc/ARCHITECTURE_GUIDE.md +73 -0
- package/doc/CHEATSHEET.md +34 -29
- package/doc/FEATURES.md +3 -3
- package/doc/STRUCTURE.md +45 -27
- package/lib/bootstrap.ts +145 -0
- package/{src → lib}/core/server.ts +21 -6
- package/{src → lib}/middleware/auth.ts +9 -2
- package/{src → lib}/middleware/error.ts +5 -0
- package/package.json +7 -5
- package/prisma/seed.ts +1 -1
- package/scripts/verify-rbac-functional.js +187 -0
- package/src/controllers/authController.ts +19 -6
- package/src/controllers/petController.ts +5 -5
- package/src/controllers/rbacController.ts +49 -12
- package/src/routes/auth.ts +2 -2
- package/src/routes/index.ts +3 -3
- package/src/routes/pets.ts +2 -2
- package/src/routes/rbac.ts +19 -32
- package/tsconfig.json +2 -0
- package/src/index.ts +0 -106
- /package/{src → lib}/core/database.ts +0 -0
- /package/{src → lib}/core/realtime.ts +0 -0
- /package/{src → lib}/core/redis.ts +0 -0
- /package/{src → lib}/core/serializer.ts +0 -0
- /package/{src → lib}/middleware/multipart.ts +0 -0
- /package/{src → lib}/middleware/rateLimit.ts +0 -0
- /package/{src → lib}/middleware/requestLogger.ts +0 -0
- /package/{src → lib}/middleware/visitor.ts +0 -0
- /package/{src → lib}/utils/logger.ts +0 -0
- /package/{src → lib}/utils/pagination.ts +0 -0
- /package/{src → lib}/utils/response.ts +0 -0
- /package/{src → lib}/utils/validator.ts +0 -0
package/bin/index.js
CHANGED
|
@@ -6,15 +6,58 @@ 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
function runDev() {
|
|
35
|
+
console.log('🚀 Starting Lapeh in development mode...');
|
|
36
|
+
try {
|
|
37
|
+
// We execute a script that requires ts-node to run lib/bootstrap.ts
|
|
38
|
+
// In a real package, this would be `node dist/lib/bootstrap.js`
|
|
39
|
+
execSync('npx nodemon --exec ts-node -r tsconfig-paths/register -e "require(\'./lib/bootstrap\').bootstrap()"', { stdio: 'inherit' });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Ignore error
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runStart() {
|
|
46
|
+
console.log('🚀 Starting Lapeh production server...');
|
|
47
|
+
const distPath = path.join(process.cwd(), 'dist/lib/bootstrap.js');
|
|
48
|
+
// For production, we assume built files
|
|
49
|
+
const cmd = `node -e "require('${distPath.replace(/\\/g, '/')}').bootstrap()"`;
|
|
50
|
+
execSync(cmd, {
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
env: { ...process.env, NODE_ENV: 'production' }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runBuild() {
|
|
57
|
+
console.log('🛠️ Building Lapeh project...');
|
|
58
|
+
execSync('npm run prisma:generate', { stdio: 'inherit' });
|
|
59
|
+
execSync('npx tsc && npx tsc-alias', { stdio: 'inherit' });
|
|
60
|
+
console.log('✅ Build complete.');
|
|
18
61
|
}
|
|
19
62
|
|
|
20
63
|
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
|
|
8
|
-
|
|
|
9
|
-
| **`npm run dev`**
|
|
10
|
-
| **`npm run typecheck`**
|
|
11
|
-
| **`npm run lint`**
|
|
12
|
-
| **`npm run lint:fix`**
|
|
13
|
-
| **`npm run make:module <Name>`**
|
|
14
|
-
| **`npm run make:controller <Name>`** | Buat Controller saja.
|
|
15
|
-
| **`npm run make:model <Name>`**
|
|
16
|
-
| **`npm run prisma:migrate`**
|
|
17
|
-
| **`npm run db:studio`**
|
|
18
|
-
| **`npm run db:seed`**
|
|
19
|
-
| **`npm run db:reset`**
|
|
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
|
|
26
|
-
|
|
|
27
|
-
| `required`
|
|
28
|
-
| `string`
|
|
29
|
-
| `number`
|
|
30
|
-
| `email`
|
|
31
|
-
| `min:X`
|
|
32
|
-
| `max:X`
|
|
33
|
-
| `unique:table,col` | Cek unik di DB.
|
|
34
|
-
| `exists:table,col` | Cek exist di DB.
|
|
35
|
-
| `image`
|
|
36
|
-
| `mimes:types`
|
|
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);
|
|
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 "
|
|
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:**
|
|
9
|
+
**Lokasi:** `@lapeh/utils/validator`
|
|
10
10
|
|
|
11
11
|
### Penggunaan Dasar
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { Validator } from "
|
|
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 "
|
|
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
|
|
8
|
-
|
|
|
9
|
-
| `bin/`
|
|
10
|
-
| `doc/`
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
|
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
package/lib/bootstrap.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lapeh",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "Framework API Express yang siap pakai (Standardized)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
},
|
|
17
17
|
"types": "dist/src/index.d.ts",
|
|
18
18
|
"scripts": {
|
|
19
|
-
"dev": "node
|
|
19
|
+
"dev": "node bin/index.js dev",
|
|
20
20
|
"first": "node scripts/init-project.js",
|
|
21
21
|
"prebuild": "npm run prisma:generate",
|
|
22
|
-
"build": "
|
|
22
|
+
"build": "node bin/index.js build",
|
|
23
23
|
"prestart": "npm run prisma:generate",
|
|
24
|
-
"start": "node
|
|
24
|
+
"start": "node bin/index.js start",
|
|
25
25
|
"prestart:prod": "npm run prisma:generate",
|
|
26
|
-
"start:prod": "
|
|
26
|
+
"start:prod": "node bin/index.js start",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
28
|
"lint": "eslint .",
|
|
29
29
|
"lint:fix": "eslint . --fix",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@prisma/adapter-pg": "7.2.0",
|
|
68
68
|
"@prisma/client": "7.2.0",
|
|
69
69
|
"bcryptjs": "3.0.3",
|
|
70
|
+
"compression": "^1.8.1",
|
|
70
71
|
"cors": "2.8.5",
|
|
71
72
|
"dotenv": "17.2.3",
|
|
72
73
|
"express": "5.2.1",
|
|
@@ -88,6 +89,7 @@
|
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@eslint/js": "^9.39.2",
|
|
90
91
|
"@types/bcryptjs": "2.4.6",
|
|
92
|
+
"@types/compression": "^1.8.1",
|
|
91
93
|
"@types/cors": "2.8.19",
|
|
92
94
|
"@types/express": "5.0.6",
|
|
93
95
|
"@types/jsonwebtoken": "9.0.10",
|