lapeh 2.2.0 → 2.2.2
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/doc/CHANGELOG.md +38 -0
- package/doc/PERFORMANCE.md +91 -0
- package/eslint.config.mjs +26 -0
- package/framework.md +168 -113
- package/nodemon.json +1 -1
- package/package.json +24 -11
- package/prisma/seed.ts +0 -1
- package/readme.md +26 -10
- package/src/controllers/authController.ts +145 -39
- package/src/controllers/petController.ts +70 -16
- package/src/controllers/rbacController.ts +82 -9
- package/src/core/redis.ts +5 -4
- package/src/core/serializer.ts +63 -0
- package/src/middleware/auth.ts +0 -1
- package/src/middleware/rateLimit.ts +1 -1
- package/src/routes/auth.ts +3 -3
- package/src/utils/response.ts +21 -0
- package/tsconfig.json +17 -12
package/doc/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
File ini mencatat semua perubahan, pembaruan, dan perbaikan yang dilakukan pada framework Lapeh, diurutkan berdasarkan tanggal.
|
|
4
4
|
|
|
5
|
+
## [2025-12-27] - Code Quality & Standardization Update
|
|
6
|
+
|
|
7
|
+
### 🚀 Fitur & Standarisasi
|
|
8
|
+
|
|
9
|
+
- **Standardized Import Paths**:
|
|
10
|
+
- Implementasi path alias `@/` untuk import yang lebih bersih (e.g., `import { prisma } from "@/core/database"`).
|
|
11
|
+
- Penghapusan penggunaan relative paths yang dalam (`../../../`).
|
|
12
|
+
- Konfigurasi `tsconfig.json` tanpa `baseUrl` (mengikuti standar TypeScript 6.0+).
|
|
13
|
+
- **Strict Linting & Code Quality**:
|
|
14
|
+
- Implementasi aturan **ESLint** ketat untuk mencegah "Dead Code".
|
|
15
|
+
- Error otomatis untuk variabel, parameter, dan import yang tidak digunakan (`no-unused-vars`).
|
|
16
|
+
- Script `npm run lint` dan `npm run lint:fix` untuk pembersihan kode otomatis.
|
|
17
|
+
- **Fastify-Style Standardization**:
|
|
18
|
+
- Penerapan standar respon cepat (`sendFastSuccess`) di seluruh controller (`AuthController`, `RbacController`, `PetController`).
|
|
19
|
+
- Penggunaan **Schema-based Serialization** untuk performa JSON maksimal.
|
|
20
|
+
- Konversi otomatis `BigInt` ke `string` dalam respon JSON.
|
|
21
|
+
|
|
22
|
+
## [2025-12-27] - High Performance & Scalability Update
|
|
23
|
+
|
|
24
|
+
### 🚀 Fitur Baru
|
|
25
|
+
|
|
26
|
+
- **High Performance Serialization (Fastify-Style)**:
|
|
27
|
+
- Implementasi `fast-json-stringify` untuk serialisasi JSON super cepat (2x-3x lebih cepat dari `JSON.stringify`).
|
|
28
|
+
- Helper `sendFastSuccess` di `src/utils/response.ts` untuk mem-bypass overhead Express.
|
|
29
|
+
- Caching schema serializer otomatis di `src/core/serializer.ts`.
|
|
30
|
+
- **Scalability & Clustering**:
|
|
31
|
+
- Dukungan **Load Balancing** dengan Nginx.
|
|
32
|
+
- Dukungan **Redis Clustering** untuk Rate Limiter (`rate-limit-redis`).
|
|
33
|
+
- File konfigurasi `docker-compose.cluster.yml` untuk simulasi cluster lokal (1 Nginx + 2 App Instances + 1 Redis).
|
|
34
|
+
- **Smart Error Handling**:
|
|
35
|
+
- Deteksi otomatis port bentrok (`EADDRINUSE`) saat startup.
|
|
36
|
+
- Memberikan saran command _copy-paste_ untuk mematikan process yang memblokir port (support Windows, Mac, Linux).
|
|
37
|
+
- **SEO Optimization**:
|
|
38
|
+
- Update metadata `package.json` dan `README.md` agar framework lebih mudah ditemukan di Google/NPM.
|
|
39
|
+
|
|
5
40
|
## [2025-12-27] - Pembaruan Struktur & Validasi
|
|
6
41
|
|
|
7
42
|
### 🚀 Fitur Baru
|
|
43
|
+
|
|
8
44
|
- **Laravel-style Validator**:
|
|
9
45
|
- Implementasi utility `Validator` baru di `src/utils/validator.ts` yang meniru gaya validasi Laravel.
|
|
10
46
|
- Mendukung rule string seperti `required|string|min:3|email`.
|
|
@@ -24,6 +60,7 @@ File ini mencatat semua perubahan, pembaruan, dan perbaikan yang dilakukan pada
|
|
|
24
60
|
- `npx lapeh <project-name> --full` kini otomatis menjalankan server dev setelah instalasi selesai, sehingga user bisa langsung melihat hasil tanpa mengetik perintah tambahan.
|
|
25
61
|
|
|
26
62
|
### 🛠️ Perbaikan & Refactoring
|
|
63
|
+
|
|
27
64
|
- **Controller Refactoring**:
|
|
28
65
|
- `AuthController`: Migrasi ke `Validator` baru, termasuk validasi upload avatar.
|
|
29
66
|
- `PetController`: Migrasi ke `Validator` baru.
|
|
@@ -33,6 +70,7 @@ File ini mencatat semua perubahan, pembaruan, dan perbaikan yang dilakukan pada
|
|
|
33
70
|
- Penghapusan file duplikat/lama di root `src/` setelah migrasi ke `src/core/`.
|
|
34
71
|
|
|
35
72
|
### 📝 Catatan Teknis
|
|
73
|
+
|
|
36
74
|
- **Validator Async**: Method `fails()`, `passes()`, dan `validated()` kini bersifat `async` untuk mendukung pengecekan database (`unique`).
|
|
37
75
|
- **Type Safety**: Semua perubahan telah diverifikasi dengan `npm run typecheck`.
|
|
38
76
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Panduan Performa & Skalabilitas Lapeh Framework
|
|
2
|
+
|
|
3
|
+
Dokumen ini menjelaskan cara memaksimalkan performa aplikasi Lapeh Anda menggunakan fitur-fitur canggih seperti Fast-Serialization dan Clustering.
|
|
4
|
+
|
|
5
|
+
## 1. High Performance Serialization (Fastify-Style)
|
|
6
|
+
|
|
7
|
+
Express secara default menggunakan `JSON.stringify()` yang lambat karena harus memeriksa tipe data setiap field secara runtime. Lapeh mengadopsi teknik **Schema Based Serialization** (seperti Fastify) yang bisa meningkatkan throughput JSON hingga **2x-3x lipat**.
|
|
8
|
+
|
|
9
|
+
### Cara Penggunaan
|
|
10
|
+
|
|
11
|
+
Gunakan `sendFastSuccess` di controller Anda untuk endpoint yang membutuhkan performa tinggi (misalnya: list data yang besar).
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Request, Response } from "express";
|
|
15
|
+
import { getSerializer, createResponseSchema } from "../core/serializer";
|
|
16
|
+
import { sendFastSuccess } from "../utils/response";
|
|
17
|
+
|
|
18
|
+
// 1. Definisikan Schema Output (JSON Schema Standard)
|
|
19
|
+
const userSchema = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
id: { type: "integer" },
|
|
23
|
+
name: { type: "string" },
|
|
24
|
+
email: { type: "string" },
|
|
25
|
+
// Password tidak dimasukkan, jadi otomatis tidak akan terkirim (aman!)
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 2. Compile Serializer (Otomatis dicache oleh framework)
|
|
30
|
+
const userListSerializer = getSerializer("user-list", createResponseSchema({
|
|
31
|
+
type: "array",
|
|
32
|
+
items: userSchema
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
export async function getUsers(req: Request, res: Response) {
|
|
36
|
+
const users = await prisma.users.findMany();
|
|
37
|
+
|
|
38
|
+
// 3. Kirim response super cepat
|
|
39
|
+
return sendFastSuccess(res, 200, userListSerializer, {
|
|
40
|
+
status: "success",
|
|
41
|
+
message: "Data fetched",
|
|
42
|
+
data: users
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Horizontal Scaling (Load Balancer & Cluster)
|
|
50
|
+
|
|
51
|
+
Lapeh dirancang untuk siap di-scale secara horizontal (menambah jumlah server, bukan memperbesar spesifikasi server).
|
|
52
|
+
|
|
53
|
+
### Arsitektur Cluster
|
|
54
|
+
- **Nginx**: Bertindak sebagai Load Balancer yang membagi trafik ke server-server aplikasi.
|
|
55
|
+
- **Redis**: Menyimpan Session, Rate Limit, dan Cache agar bisa diakses oleh semua server (Shared State).
|
|
56
|
+
- **App Instances**: Beberapa instance aplikasi Lapeh yang berjalan paralel.
|
|
57
|
+
|
|
58
|
+
### Cara Menjalankan Cluster (Docker)
|
|
59
|
+
|
|
60
|
+
Kami telah menyediakan konfigurasi siap pakai di `docker-compose.cluster.yml`.
|
|
61
|
+
|
|
62
|
+
1. **Build & Run Cluster**:
|
|
63
|
+
```bash
|
|
64
|
+
docker-compose -f docker-compose.cluster.yml up --build
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
2. **Akses Aplikasi**:
|
|
68
|
+
Buka `http://localhost:8080`.
|
|
69
|
+
Nginx akan otomatis membagi request Anda ke `app-1` atau `app-2`.
|
|
70
|
+
|
|
71
|
+
3. **Cek Status**:
|
|
72
|
+
```bash
|
|
73
|
+
docker-compose -f docker-compose.cluster.yml ps
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Konfigurasi Rate Limiter Terdistribusi
|
|
77
|
+
Middleware `src/middleware/rateLimit.ts` telah diupdate untuk menggunakan Redis Store.
|
|
78
|
+
Ini artinya jika User A terkena limit di Server 1, dia juga akan terblokir di Server 2.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// src/middleware/rateLimit.ts
|
|
82
|
+
store: redis ? new RedisStore({ sendCommand: ... }) : undefined
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. Tips Optimasi Lainnya
|
|
88
|
+
|
|
89
|
+
- **Gunakan `.lean()` / Select**: Saat query database, selalu pilih field yang dibutuhkan saja.
|
|
90
|
+
- **Compression**: Aktifkan gzip/brotli di Nginx (sudah ada di config default Nginx umumnya).
|
|
91
|
+
- **Keep-Alive**: Gunakan koneksi database yang persistent (sudah dihandle oleh Prisma).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import tseslint from "typescript-eslint";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{ files: ["**/*.{js,mjs,cjs,ts}"] },
|
|
7
|
+
{ languageOptions: { globals: globals.node } },
|
|
8
|
+
pluginJs.configs.recommended,
|
|
9
|
+
...tseslint.configs.recommended,
|
|
10
|
+
{
|
|
11
|
+
rules: {
|
|
12
|
+
"@typescript-eslint/no-unused-vars": [
|
|
13
|
+
"error",
|
|
14
|
+
{
|
|
15
|
+
"argsIgnorePattern": "^_",
|
|
16
|
+
"varsIgnorePattern": "^_",
|
|
17
|
+
"caughtErrorsIgnorePattern": "^_"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"@typescript-eslint/no-explicit-any": "warn"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
ignores: ["dist/", "node_modules/", "generated/", "scripts/"]
|
|
25
|
+
}
|
|
26
|
+
];
|
package/framework.md
CHANGED
|
@@ -1,113 +1,168 @@
|
|
|
1
|
-
# Lapeh Framework
|
|
2
|
-
|
|
3
|
-
## Quick Start
|
|
4
|
-
|
|
5
|
-
Untuk memulai project ini (Setup awal):
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm i
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm run first
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Perintah di atas akan secara otomatis melakukan:
|
|
16
|
-
|
|
17
|
-
1. Copy `.env.example` ke `.env`
|
|
18
|
-
2. Install dependencies (`npm install`)
|
|
19
|
-
3. Generate JWT Secret baru di `.env`
|
|
20
|
-
4. Setup database (Migrate)
|
|
21
|
-
5. Menjalankan Database Seeder
|
|
22
|
-
|
|
23
|
-
Setelah selesai, Anda bisa langsung menjalankan project:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm run dev
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Akun Default
|
|
30
|
-
|
|
31
|
-
Jika seeder dijalankan (via `npm run first` atau `npm run db:seed`), gunakan akun berikut:
|
|
32
|
-
|
|
33
|
-
- **Super Admin**: `sa@sa.com` / `string`
|
|
34
|
-
- **Admin**: `a@a.com` / `string`
|
|
35
|
-
- **User**: `u@u.com` / `string`
|
|
36
|
-
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
# Lapeh Framework
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
Untuk memulai project ini (Setup awal):
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run first
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Perintah di atas akan secara otomatis melakukan:
|
|
16
|
+
|
|
17
|
+
1. Copy `.env.example` ke `.env`
|
|
18
|
+
2. Install dependencies (`npm install`)
|
|
19
|
+
3. Generate JWT Secret baru di `.env`
|
|
20
|
+
4. Setup database (Migrate)
|
|
21
|
+
5. Menjalankan Database Seeder
|
|
22
|
+
|
|
23
|
+
Setelah selesai, Anda bisa langsung menjalankan project:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Akun Default
|
|
30
|
+
|
|
31
|
+
Jika seeder dijalankan (via `npm run first` atau `npm run db:seed`), gunakan akun berikut:
|
|
32
|
+
|
|
33
|
+
- **Super Admin**: `sa@sa.com` / `string`
|
|
34
|
+
- **Admin**: `a@a.com` / `string`
|
|
35
|
+
- **User**: `u@u.com` / `string`
|
|
36
|
+
|
|
37
|
+
## Code Standards & Best Practices (Baru)
|
|
38
|
+
|
|
39
|
+
### 1. Import Path Aliases
|
|
40
|
+
Gunakan alias `@/` untuk mengimpor module dari folder `src/`. Hindari relative path yang panjang seperti `../../utils/response`.
|
|
41
|
+
|
|
42
|
+
**Contoh:**
|
|
43
|
+
```typescript
|
|
44
|
+
// ✅ Benar (Recommended)
|
|
45
|
+
import { prisma } from "@/core/database";
|
|
46
|
+
import { sendSuccess } from "@/utils/response";
|
|
47
|
+
|
|
48
|
+
// ❌ Salah (Legacy)
|
|
49
|
+
import { prisma } from "../core/database";
|
|
50
|
+
import { sendSuccess } from "../../utils/response";
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Strict Linting (Dead Code Elimination)
|
|
54
|
+
Framework ini menerapkan aturan linter yang ketat untuk menjaga kebersihan kode. Variabel, parameter, atau import yang tidak digunakan akan menyebabkan error.
|
|
55
|
+
|
|
56
|
+
- **Variabel tidak terpakai**: Hapus atau beri prefix `_` (underscore).
|
|
57
|
+
```typescript
|
|
58
|
+
// ✅ Benar
|
|
59
|
+
const _unusedVariable = 123;
|
|
60
|
+
function example(_req: Request, res: Response) { ... }
|
|
61
|
+
|
|
62
|
+
// ❌ Error
|
|
63
|
+
const unusedVariable = 123;
|
|
64
|
+
function example(req: Request, res: Response) { ... } // jika req tidak dipakai
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. High Performance Response (Fastify-Style)
|
|
68
|
+
Untuk endpoint dengan throughput tinggi (GET lists, data besar), gunakan `sendFastSuccess` dengan JSON Schema serializer. Ini 2-3x lebih cepat dari `res.json` standar Express.
|
|
69
|
+
|
|
70
|
+
**Langkah-langkah:**
|
|
71
|
+
|
|
72
|
+
1. **Definisikan Schema** (sesuai field Prisma):
|
|
73
|
+
```typescript
|
|
74
|
+
const userSchema = {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
id: { type: "string" }, // BigInt otomatis dicovert ke string
|
|
78
|
+
name: { type: "string" },
|
|
79
|
+
email: { type: "string" }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. **Buat Serializer**:
|
|
85
|
+
```typescript
|
|
86
|
+
import { getSerializer, createResponseSchema } from "@/core/serializer";
|
|
87
|
+
|
|
88
|
+
const userSerializer = getSerializer("user-detail", createResponseSchema(userSchema));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
3. **Gunakan di Controller**:
|
|
92
|
+
```typescript
|
|
93
|
+
import { sendFastSuccess } from "@/utils/response";
|
|
94
|
+
|
|
95
|
+
export async function getUser(req, res) {
|
|
96
|
+
const user = await prisma.user.findFirst();
|
|
97
|
+
sendFastSuccess(res, 200, userSerializer, {
|
|
98
|
+
status: "success",
|
|
99
|
+
message: "User found",
|
|
100
|
+
data: user
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Database Workflow (Prisma)
|
|
106
|
+
|
|
107
|
+
Framework ini menggunakan **Prisma ORM** dengan struktur schema yang modular (dipecah per file). Berikut adalah panduan lengkap dari Development hingga Deployment.
|
|
108
|
+
|
|
109
|
+
### 1. Development (Lokal)
|
|
110
|
+
|
|
111
|
+
Saat mengembangkan aplikasi di local environment:
|
|
112
|
+
|
|
113
|
+
**a. Mengupdate Schema Database**
|
|
114
|
+
Jika Anda mengubah file schema di `src/models/*.prisma` atau konfigurasi di `prisma/base.prisma.template`:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm run prisma:migrate
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
_Perintah ini akan menggabungkan semua file schema, membuat file migrasi baru, menerapkan ke database lokal, dan men-generate ulang Prisma Client._
|
|
121
|
+
|
|
122
|
+
**b. Melihat/Edit Data (GUI)**
|
|
123
|
+
Untuk membuka dashboard visual database:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm run db:studio
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**c. Mengisi Data Awal (Seeding)**
|
|
130
|
+
Jika Anda butuh data dummy atau data awal (seperti roles/permissions):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run db:seed
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**d. Reset Database Total**
|
|
137
|
+
Jika database berantakan dan ingin mengulang dari awal (HATI-HATI: Menghapus semua data):
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run db:reset
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
_Perintah ini akan menghapus database, membuat ulang schema dari awal, dan otomatis menjalankan seeder._
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 2. Deployment (Production)
|
|
148
|
+
|
|
149
|
+
Saat deploy ke server production:
|
|
150
|
+
|
|
151
|
+
**a. Setup Awal**
|
|
152
|
+
Pastikan `.env` di production sudah disetup dengan benar (DATABASE_URL, dll).
|
|
153
|
+
|
|
154
|
+
**b. Menerapkan Migrasi**
|
|
155
|
+
Jangan gunakan `migrate dev` di production. Gunakan perintah ini:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm run prisma:deploy
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
_Perintah ini hanya akan menerapkan file migrasi yang sudah ada ke database production tanpa mereset data atau meminta konfirmasi interaktif._
|
|
162
|
+
|
|
163
|
+
**c. Generate Client (Opsional)**
|
|
164
|
+
Biasanya dilakukan otomatis saat `npm install` (karena `postinstall`), tapi jika perlu manual:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run prisma:generate
|
|
168
|
+
```
|
package/nodemon.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lapeh",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Framework API Express yang siap pakai (Standardized)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
"dev": "node scripts/check-update.js && nodemon src/index.ts",
|
|
20
20
|
"first": "node scripts/init-project.js",
|
|
21
21
|
"prebuild": "npm run prisma:generate",
|
|
22
|
-
"build": "tsc",
|
|
22
|
+
"build": "tsc && tsc-alias",
|
|
23
23
|
"prestart": "npm run prisma:generate",
|
|
24
24
|
"start": "node dist/src/index.js",
|
|
25
25
|
"prestart:prod": "npm run prisma:generate",
|
|
26
26
|
"start:prod": "NODE_ENV=production node dist/src/index.js",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
|
+
"lint": "eslint .",
|
|
29
|
+
"lint:fix": "eslint . --fix",
|
|
28
30
|
"prisma:generate": "node scripts/compile-schema.js && prisma generate",
|
|
29
31
|
"prisma:migrate": "node scripts/compile-schema.js && prisma migrate dev",
|
|
30
32
|
"prisma:deploy": "node scripts/compile-schema.js && prisma migrate deploy",
|
|
@@ -39,15 +41,19 @@
|
|
|
39
41
|
"config:clear": "node scripts/config-clear.js"
|
|
40
42
|
},
|
|
41
43
|
"keywords": [
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"framework",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
44
|
+
"nodejs-framework",
|
|
45
|
+
"typescript-framework",
|
|
46
|
+
"express-framework",
|
|
47
|
+
"backend-framework",
|
|
48
|
+
"rest-api",
|
|
49
|
+
"prisma-orm",
|
|
50
|
+
"production-ready",
|
|
51
|
+
"api-generator",
|
|
47
52
|
"boilerplate",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
53
|
+
"starter-kit",
|
|
54
|
+
"mvc",
|
|
55
|
+
"cli",
|
|
56
|
+
"lapeh",
|
|
51
57
|
"roby-ajo",
|
|
52
58
|
"ajo-roby",
|
|
53
59
|
"roby-karti-s",
|
|
@@ -65,6 +71,7 @@
|
|
|
65
71
|
"dotenv": "17.2.3",
|
|
66
72
|
"express": "5.2.1",
|
|
67
73
|
"express-rate-limit": "8.2.1",
|
|
74
|
+
"fast-json-stringify": "^6.1.1",
|
|
68
75
|
"helmet": "8.1.0",
|
|
69
76
|
"ioredis": "5.8.2",
|
|
70
77
|
"ioredis-mock": "^8.13.1",
|
|
@@ -79,6 +86,7 @@
|
|
|
79
86
|
"zod": "3.23.8"
|
|
80
87
|
},
|
|
81
88
|
"devDependencies": {
|
|
89
|
+
"@eslint/js": "^9.39.2",
|
|
82
90
|
"@types/bcryptjs": "2.4.6",
|
|
83
91
|
"@types/cors": "2.8.19",
|
|
84
92
|
"@types/express": "5.0.6",
|
|
@@ -87,9 +95,14 @@
|
|
|
87
95
|
"@types/node": "25.0.3",
|
|
88
96
|
"@types/pg": "8.16.0",
|
|
89
97
|
"@types/uuid": "10.0.0",
|
|
98
|
+
"eslint": "^9.39.2",
|
|
99
|
+
"globals": "^16.5.0",
|
|
90
100
|
"nodemon": "3.1.11",
|
|
91
101
|
"prisma": "7.2.0",
|
|
92
102
|
"ts-node": "10.9.2",
|
|
93
|
-
"
|
|
103
|
+
"tsc-alias": "^1.8.16",
|
|
104
|
+
"tsconfig-paths": "^4.2.0",
|
|
105
|
+
"typescript": "5.9.3",
|
|
106
|
+
"typescript-eslint": "^8.50.1"
|
|
94
107
|
}
|
|
95
108
|
}
|
package/prisma/seed.ts
CHANGED
package/readme.md
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
# Lapeh Framework
|
|
1
|
+
# Lapeh Framework - Modern Node.js & TypeScript API Framework
|
|
2
2
|
|
|
3
|
-
**Lapeh** adalah framework berbasis Express
|
|
3
|
+
**Lapeh** adalah framework **Node.js** berbasis **Express** dan **TypeScript** yang dirancang untuk kecepatan dan skalabilitas. Menggabungkan fleksibilitas Express dengan struktur solid ala **Laravel** dan **NestJS**, Lapeh memberikan pengalaman development **REST API** yang cepat, terstandarisasi, dan siap produksi.
|
|
4
|
+
|
|
5
|
+
Cocok untuk developer yang mencari **Express boilerplate** dengan fitur lengkap: Prisma ORM, Authentication, RBAC, dan Zero-Config Redis.
|
|
4
6
|
|
|
5
7
|
## 🚀 Fitur Utama
|
|
6
8
|
|
|
7
|
-
- **
|
|
8
|
-
- **TypeScript
|
|
9
|
-
- **Prisma ORM**:
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
9
|
+
- **Production Ready**: Struktur folder modular (MVC) yang mudah dikembangkan.
|
|
10
|
+
- **TypeScript First**: Full type-safety untuk mengurangi runtime error.
|
|
11
|
+
- **Prisma ORM Integration**: Database modern dengan dukungan PostgreSQL dan MySQL.
|
|
12
|
+
- **Laravel-style Structure**: Controller, Service, dan Route yang terpisah rapi.
|
|
13
|
+
- **Auto CLI Generator**: Buat modul, model, dan controller dengan satu perintah.
|
|
14
|
+
- **Smart Caching**: Otomatis menggunakan Redis jika tersedia, fallback ke in-memory jika tidak.
|
|
15
|
+
- **Secure by Default**: Dilengkapi Helmet, Rate Limiting, CORS, dan JWT Auth.
|
|
16
|
+
- **Robust Validation**: Validasi request otomatis menggunakan Zod.
|
|
17
|
+
- **High Performance**: Mendukung Fast-Serialization (Fastify-style) untuk response JSON super cepat.
|
|
18
|
+
- **Scalable**: Siap untuk deployment Cluster/Load Balancer dengan Redis Store.
|
|
19
|
+
|
|
20
|
+
## 📚 Dokumentasi Lengkap
|
|
21
|
+
|
|
22
|
+
- [Panduan Instalasi & CLI](doc/CLI.md) (Coming Soon)
|
|
23
|
+
- [Panduan Performa & Scaling](doc/PERFORMANCE.md) ⚡ _(Baru)_
|
|
24
|
+
- [Changelog](doc/CHANGELOG.md)
|
|
15
25
|
|
|
16
26
|
## 📦 Instalasi & Penggunaan
|
|
17
27
|
|
|
@@ -249,6 +259,7 @@ MIT
|
|
|
249
259
|
## 🚀 Deployment Guide
|
|
250
260
|
|
|
251
261
|
### 1) Build & Generate Prisma Client (Otomatis)
|
|
262
|
+
|
|
252
263
|
- Build: `npm run build`
|
|
253
264
|
- Start (dev): `npm run start`
|
|
254
265
|
- Start (prod): `npm run start:prod`
|
|
@@ -256,6 +267,7 @@ MIT
|
|
|
256
267
|
- `prebuild`, `prestart`, dan `prestart:prod` akan memanggil `npm run prisma:generate` sehingga Prisma Client selalu tersedia tanpa error.
|
|
257
268
|
|
|
258
269
|
### 2) Production Environment
|
|
270
|
+
|
|
259
271
|
- Pastikan `.env` berisi kredensial production:
|
|
260
272
|
- `DATABASE_URL` dan `DATABASE_PROVIDER` (mysql/postgresql)
|
|
261
273
|
- `JWT_SECRET` (gunakan `npm run generate:jwt` untuk mengganti)
|
|
@@ -266,6 +278,7 @@ npm run prisma:deploy
|
|
|
266
278
|
```
|
|
267
279
|
|
|
268
280
|
### 3) Menjalankan dengan PM2
|
|
281
|
+
|
|
269
282
|
- Install PM2:
|
|
270
283
|
|
|
271
284
|
```bash
|
|
@@ -294,6 +307,7 @@ pm2 restart lapeh-api
|
|
|
294
307
|
```
|
|
295
308
|
|
|
296
309
|
### 4) Nginx Reverse Proxy (Recommended)
|
|
310
|
+
|
|
297
311
|
- Buat server block `/etc/nginx/sites-available/lapeh`:
|
|
298
312
|
|
|
299
313
|
```nginx
|
|
@@ -327,6 +341,7 @@ sudo certbot --nginx -d example.com
|
|
|
327
341
|
```
|
|
328
342
|
|
|
329
343
|
### 5) Apache 2 Reverse Proxy (Alternatif)
|
|
344
|
+
|
|
330
345
|
- Enable modul proxy:
|
|
331
346
|
|
|
332
347
|
```bash
|
|
@@ -360,6 +375,7 @@ sudo systemctl reload apache2
|
|
|
360
375
|
```
|
|
361
376
|
|
|
362
377
|
### 6) Checklist Produksi
|
|
378
|
+
|
|
363
379
|
- `npm run prisma:deploy` sukses dan tabel terbentuk
|
|
364
380
|
- `pm2 status` menunjukkan proses hidup
|
|
365
381
|
- Proxy (Nginx/Apache) menuju port aplikasi (default 4000)
|
|
@@ -1,13 +1,83 @@
|
|
|
1
|
-
import { Request, Response } from "express";
|
|
2
|
-
import bcrypt from "bcryptjs";
|
|
3
|
-
import jwt from "jsonwebtoken";
|
|
4
|
-
import { v4 as uuidv4 } from "uuid";
|
|
5
|
-
import { prisma } from "
|
|
6
|
-
import { sendSuccess, sendError } from "
|
|
7
|
-
import { Validator } from "
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { prisma } from "@/core/database";
|
|
6
|
+
import { sendSuccess, sendError, sendFastSuccess } from "@/utils/response";
|
|
7
|
+
import { Validator } from "@/utils/validator";
|
|
8
|
+
import { getSerializer, createResponseSchema } from "@/core/serializer";
|
|
8
9
|
|
|
9
10
|
export const ACCESS_TOKEN_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
|
|
10
11
|
|
|
12
|
+
// --- Serializers ---
|
|
13
|
+
|
|
14
|
+
const registerSchema = {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
id: { type: "string" },
|
|
18
|
+
email: { type: "string" },
|
|
19
|
+
name: { type: "string" },
|
|
20
|
+
role: { type: "string" },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const loginSchema = {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
token: { type: "string" },
|
|
28
|
+
refreshToken: { type: "string" },
|
|
29
|
+
expiresIn: { type: "integer" },
|
|
30
|
+
expiresAt: { type: "string" },
|
|
31
|
+
name: { type: "string" },
|
|
32
|
+
role: { type: "string" },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const userProfileSchema = {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
id: { type: "string" },
|
|
40
|
+
name: { type: "string" },
|
|
41
|
+
email: { type: "string" },
|
|
42
|
+
role: { type: "string" },
|
|
43
|
+
avatar: { type: "string", nullable: true },
|
|
44
|
+
avatar_url: { type: "string", nullable: true },
|
|
45
|
+
email_verified_at: { type: "string", format: "date-time", nullable: true },
|
|
46
|
+
created_at: { type: "string", format: "date-time", nullable: true },
|
|
47
|
+
updated_at: { type: "string", format: "date-time", nullable: true },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const refreshTokenSchema = {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
token: { type: "string" },
|
|
55
|
+
expiresIn: { type: "integer" },
|
|
56
|
+
expiresAt: { type: "string" },
|
|
57
|
+
name: { type: "string" },
|
|
58
|
+
role: { type: "string" },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const registerSerializer = getSerializer(
|
|
63
|
+
"auth-register",
|
|
64
|
+
createResponseSchema(registerSchema)
|
|
65
|
+
);
|
|
66
|
+
const loginSerializer = getSerializer(
|
|
67
|
+
"auth-login",
|
|
68
|
+
createResponseSchema(loginSchema)
|
|
69
|
+
);
|
|
70
|
+
const userProfileSerializer = getSerializer(
|
|
71
|
+
"auth-profile",
|
|
72
|
+
createResponseSchema(userProfileSchema)
|
|
73
|
+
);
|
|
74
|
+
const refreshTokenSerializer = getSerializer(
|
|
75
|
+
"auth-refresh",
|
|
76
|
+
createResponseSchema(refreshTokenSchema)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// --- Controllers ---
|
|
80
|
+
|
|
11
81
|
export async function register(req: Request, res: Response) {
|
|
12
82
|
const validator = Validator.make(req.body || {}, {
|
|
13
83
|
email: "required|email|unique:users,email",
|
|
@@ -47,11 +117,15 @@ export async function register(req: Request, res: Response) {
|
|
|
47
117
|
});
|
|
48
118
|
}
|
|
49
119
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
120
|
+
sendFastSuccess(res, 200, registerSerializer, {
|
|
121
|
+
status: "success",
|
|
122
|
+
message: "Registration successful",
|
|
123
|
+
data: {
|
|
124
|
+
id: user.id.toString(),
|
|
125
|
+
email: user.email,
|
|
126
|
+
name: user.name,
|
|
127
|
+
role: defaultRole ? defaultRole.slug : "user",
|
|
128
|
+
},
|
|
55
129
|
});
|
|
56
130
|
}
|
|
57
131
|
|
|
@@ -119,13 +193,17 @@ export async function login(req: Request, res: Response) {
|
|
|
119
193
|
secret,
|
|
120
194
|
{ expiresIn: refreshExpiresInSeconds }
|
|
121
195
|
);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
196
|
+
sendFastSuccess(res, 200, loginSerializer, {
|
|
197
|
+
status: "success",
|
|
198
|
+
message: "Login successful",
|
|
199
|
+
data: {
|
|
200
|
+
token,
|
|
201
|
+
refreshToken,
|
|
202
|
+
expiresIn: accessExpiresInSeconds,
|
|
203
|
+
expiresAt: accessExpiresAt,
|
|
204
|
+
name: user.name,
|
|
205
|
+
role: primaryUserRole,
|
|
206
|
+
},
|
|
129
207
|
});
|
|
130
208
|
}
|
|
131
209
|
|
|
@@ -150,17 +228,24 @@ export async function me(req: Request, res: Response) {
|
|
|
150
228
|
return;
|
|
151
229
|
}
|
|
152
230
|
const { password, remember_token, ...rest } = user as any;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
231
|
+
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
232
|
+
status: "success",
|
|
233
|
+
message: "User profile",
|
|
234
|
+
data: {
|
|
235
|
+
...rest,
|
|
236
|
+
id: user.id.toString(),
|
|
237
|
+
role:
|
|
238
|
+
user.user_roles && user.user_roles.length > 0 && user.user_roles[0].role
|
|
239
|
+
? user.user_roles[0].role.slug
|
|
240
|
+
: "user",
|
|
241
|
+
},
|
|
160
242
|
});
|
|
161
243
|
}
|
|
162
244
|
|
|
163
|
-
export async function logout(
|
|
245
|
+
export async function logout(_req: Request, res: Response) {
|
|
246
|
+
// In a stateless JWT setup, logout is client-side (delete token).
|
|
247
|
+
// If using a whitelist/blacklist in Redis, invalidate the token here.
|
|
248
|
+
// For now, just return success.
|
|
164
249
|
sendSuccess(res, 200, "Logout successful", null);
|
|
165
250
|
}
|
|
166
251
|
|
|
@@ -217,12 +302,16 @@ export async function refreshToken(req: Request, res: Response) {
|
|
|
217
302
|
secret,
|
|
218
303
|
{ expiresIn: accessExpiresInSeconds }
|
|
219
304
|
);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
305
|
+
sendFastSuccess(res, 200, refreshTokenSerializer, {
|
|
306
|
+
status: "success",
|
|
307
|
+
message: "Token refreshed",
|
|
308
|
+
data: {
|
|
309
|
+
token,
|
|
310
|
+
expiresIn: accessExpiresInSeconds,
|
|
311
|
+
expiresAt: accessExpiresAt,
|
|
312
|
+
name: user.name,
|
|
313
|
+
role: primaryUserRole,
|
|
314
|
+
},
|
|
226
315
|
});
|
|
227
316
|
} catch {
|
|
228
317
|
sendError(res, 401, "Invalid refresh token");
|
|
@@ -268,9 +357,21 @@ export async function updateAvatar(req: Request, res: Response) {
|
|
|
268
357
|
},
|
|
269
358
|
});
|
|
270
359
|
const { password, remember_token, ...rest } = updated as any;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
360
|
+
// Note: user_roles might not be fetched in update, so role defaults to "user" or fetched if needed.
|
|
361
|
+
// Ideally we should refetch or pass existing role.
|
|
362
|
+
// For now assuming role is preserved or handled by frontend state, but API should return it.
|
|
363
|
+
// Let's rely on nullable role or simple "user" fallback if not present in `updated`.
|
|
364
|
+
// Actually `update` returns what was updated. Relations are not included unless specified.
|
|
365
|
+
// For now we will return it compatible with userProfileSchema.
|
|
366
|
+
|
|
367
|
+
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
368
|
+
status: "success",
|
|
369
|
+
message: "Avatar updated successfully",
|
|
370
|
+
data: {
|
|
371
|
+
...rest,
|
|
372
|
+
id: updated.id.toString(),
|
|
373
|
+
role: payload.role, // Use role from JWT payload as it shouldn't change here
|
|
374
|
+
},
|
|
274
375
|
});
|
|
275
376
|
}
|
|
276
377
|
|
|
@@ -343,8 +444,13 @@ export async function updateProfile(req: Request, res: Response) {
|
|
|
343
444
|
},
|
|
344
445
|
});
|
|
345
446
|
const { password, remember_token, ...rest } = updated as any;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
447
|
+
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
448
|
+
status: "success",
|
|
449
|
+
message: "Profile updated successfully",
|
|
450
|
+
data: {
|
|
451
|
+
...rest,
|
|
452
|
+
id: updated.id.toString(),
|
|
453
|
+
role: payload.role, // Use role from JWT payload
|
|
454
|
+
},
|
|
349
455
|
});
|
|
350
456
|
}
|
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import { Request, Response } from "express";
|
|
2
|
-
import { prisma } from "
|
|
3
|
-
import { sendSuccess, sendError } from "
|
|
4
|
-
import { getPagination, buildPaginationMeta } from "
|
|
5
|
-
import { Validator } from "
|
|
2
|
+
import { prisma } from "@/core/database";
|
|
3
|
+
import { sendSuccess, sendError, sendFastSuccess } from "@/utils/response";
|
|
4
|
+
import { getPagination, buildPaginationMeta } from "@/utils/pagination";
|
|
5
|
+
import { Validator } from "@/utils/validator";
|
|
6
|
+
import {
|
|
7
|
+
getSerializer,
|
|
8
|
+
createResponseSchema,
|
|
9
|
+
createPaginatedResponseSchema,
|
|
10
|
+
} from "@/core/serializer";
|
|
11
|
+
|
|
12
|
+
// 1. Definisikan Schema Output untuk performa tinggi
|
|
13
|
+
const petSchema = {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
id: { type: "string" }, // BigInt dikonversi ke string
|
|
17
|
+
name: { type: "string" },
|
|
18
|
+
species: { type: "string" },
|
|
19
|
+
age: { type: "integer" },
|
|
20
|
+
created_at: { type: "string", format: "date-time" },
|
|
21
|
+
updated_at: { type: "string", format: "date-time" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 2. Compile Serializer
|
|
26
|
+
// Untuk Single Item
|
|
27
|
+
const petSerializer = getSerializer(
|
|
28
|
+
"pet-single",
|
|
29
|
+
createResponseSchema(petSchema)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Untuk List Item (Paginated)
|
|
33
|
+
const petListSerializer = getSerializer(
|
|
34
|
+
"pet-list",
|
|
35
|
+
createPaginatedResponseSchema(petSchema)
|
|
36
|
+
);
|
|
6
37
|
|
|
7
38
|
export async function index(req: Request, res: Response) {
|
|
8
39
|
const { page, perPage, skip, take } = getPagination(req.query);
|
|
@@ -26,6 +57,8 @@ export async function index(req: Request, res: Response) {
|
|
|
26
57
|
prisma.pets.count({ where }),
|
|
27
58
|
]);
|
|
28
59
|
|
|
60
|
+
// Kita perlu convert BigInt ke string sebelum masuk serializer
|
|
61
|
+
// Karena fast-json-stringify mengharapkan tipe data yang sesuai dengan schema
|
|
29
62
|
const serialized = data.map((item: any) => ({
|
|
30
63
|
...item,
|
|
31
64
|
id: item.id.toString(),
|
|
@@ -33,9 +66,15 @@ export async function index(req: Request, res: Response) {
|
|
|
33
66
|
|
|
34
67
|
const meta = buildPaginationMeta(page, perPage, total);
|
|
35
68
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
// Gunakan sendFastSuccess untuk performa maksimal
|
|
70
|
+
// Struktur data disesuaikan dengan createPaginatedResponseSchema: { data: [], meta: {} }
|
|
71
|
+
sendFastSuccess(res, 200, petListSerializer, {
|
|
72
|
+
status: "success",
|
|
73
|
+
message: "Pets retrieved successfully",
|
|
74
|
+
data: {
|
|
75
|
+
data: serialized,
|
|
76
|
+
meta,
|
|
77
|
+
},
|
|
39
78
|
});
|
|
40
79
|
}
|
|
41
80
|
|
|
@@ -50,9 +89,14 @@ export async function show(req: Request, res: Response) {
|
|
|
50
89
|
return;
|
|
51
90
|
}
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
// Gunakan sendFastSuccess
|
|
93
|
+
sendFastSuccess(res, 200, petSerializer, {
|
|
94
|
+
status: "success",
|
|
95
|
+
message: "Pet retrieved successfully",
|
|
96
|
+
data: {
|
|
97
|
+
...pet,
|
|
98
|
+
id: pet.id.toString(),
|
|
99
|
+
},
|
|
56
100
|
});
|
|
57
101
|
}
|
|
58
102
|
|
|
@@ -77,9 +121,14 @@ export async function store(req: Request, res: Response) {
|
|
|
77
121
|
},
|
|
78
122
|
});
|
|
79
123
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
// Gunakan sendFastSuccess
|
|
125
|
+
sendFastSuccess(res, 201, petSerializer, {
|
|
126
|
+
status: "success",
|
|
127
|
+
message: "Pet created successfully",
|
|
128
|
+
data: {
|
|
129
|
+
...pet,
|
|
130
|
+
id: pet.id.toString(),
|
|
131
|
+
},
|
|
83
132
|
});
|
|
84
133
|
}
|
|
85
134
|
|
|
@@ -114,9 +163,14 @@ export async function update(req: Request, res: Response) {
|
|
|
114
163
|
},
|
|
115
164
|
});
|
|
116
165
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
// Gunakan sendFastSuccess
|
|
167
|
+
sendFastSuccess(res, 200, petSerializer, {
|
|
168
|
+
status: "success",
|
|
169
|
+
message: "Pet updated successfully",
|
|
170
|
+
data: {
|
|
171
|
+
...updated,
|
|
172
|
+
id: updated.id.toString(),
|
|
173
|
+
},
|
|
120
174
|
});
|
|
121
175
|
}
|
|
122
176
|
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { Request, Response } from "express";
|
|
2
|
-
import { prisma } from "
|
|
3
|
-
import { sendSuccess, sendError } from "
|
|
4
|
-
import { Validator } from "
|
|
2
|
+
import { prisma } from "@/core/database";
|
|
3
|
+
import { sendSuccess, sendError, sendFastSuccess } from "@/utils/response";
|
|
4
|
+
import { Validator } from "@/utils/validator";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { getSerializer, createResponseSchema } from "@/core/serializer";
|
|
7
|
+
|
|
8
|
+
// --- Serializers ---
|
|
9
|
+
|
|
10
|
+
const roleSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
id: { type: "string" },
|
|
14
|
+
name: { type: "string" },
|
|
15
|
+
slug: { type: "string" },
|
|
16
|
+
description: { type: "string", nullable: true },
|
|
17
|
+
created_at: { type: "string", format: "date-time", nullable: true },
|
|
18
|
+
updated_at: { type: "string", format: "date-time", nullable: true },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const permissionSchema = {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
id: { type: "string" },
|
|
26
|
+
name: { type: "string" },
|
|
27
|
+
slug: { type: "string" },
|
|
28
|
+
description: { type: "string", nullable: true },
|
|
29
|
+
created_at: { type: "string", format: "date-time", nullable: true },
|
|
30
|
+
updated_at: { type: "string", format: "date-time", nullable: true },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const roleSerializer = getSerializer("role", createResponseSchema(roleSchema));
|
|
35
|
+
const roleListSerializer = getSerializer(
|
|
36
|
+
"role-list",
|
|
37
|
+
createResponseSchema({ type: "array", items: roleSchema })
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const permissionSerializer = getSerializer(
|
|
41
|
+
"permission",
|
|
42
|
+
createResponseSchema(permissionSchema)
|
|
43
|
+
);
|
|
44
|
+
const permissionListSerializer = getSerializer(
|
|
45
|
+
"permission-list",
|
|
46
|
+
createResponseSchema({ type: "array", items: permissionSchema })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// --- Controllers ---
|
|
6
50
|
|
|
7
51
|
export async function createRole(req: Request, res: Response) {
|
|
8
52
|
const validator = Validator.make(req.body || {}, {
|
|
@@ -28,14 +72,23 @@ export async function createRole(req: Request, res: Response) {
|
|
|
28
72
|
updated_at: new Date(),
|
|
29
73
|
},
|
|
30
74
|
});
|
|
31
|
-
|
|
75
|
+
sendFastSuccess(res, 201, roleSerializer, {
|
|
76
|
+
status: "success",
|
|
77
|
+
message: "Role created",
|
|
78
|
+
data: { ...role, id: role.id.toString() },
|
|
79
|
+
});
|
|
32
80
|
}
|
|
33
81
|
|
|
34
82
|
export async function listRoles(_req: Request, res: Response) {
|
|
35
83
|
const roles = await prisma.roles.findMany({
|
|
36
84
|
orderBy: { id: "asc" },
|
|
37
85
|
});
|
|
38
|
-
|
|
86
|
+
const serialized = roles.map((r: any) => ({ ...r, id: r.id.toString() }));
|
|
87
|
+
sendFastSuccess(res, 200, roleListSerializer, {
|
|
88
|
+
status: "success",
|
|
89
|
+
message: "Roles list",
|
|
90
|
+
data: serialized,
|
|
91
|
+
});
|
|
39
92
|
}
|
|
40
93
|
|
|
41
94
|
export async function updateRole(req: Request, res: Response) {
|
|
@@ -69,7 +122,11 @@ export async function updateRole(req: Request, res: Response) {
|
|
|
69
122
|
updated_at: new Date(),
|
|
70
123
|
},
|
|
71
124
|
});
|
|
72
|
-
|
|
125
|
+
sendFastSuccess(res, 200, roleSerializer, {
|
|
126
|
+
status: "success",
|
|
127
|
+
message: "Role updated",
|
|
128
|
+
data: { ...updated, id: updated.id.toString() },
|
|
129
|
+
});
|
|
73
130
|
}
|
|
74
131
|
|
|
75
132
|
export async function deleteRole(req: Request, res: Response) {
|
|
@@ -109,14 +166,26 @@ export async function createPermission(req: Request, res: Response) {
|
|
|
109
166
|
updated_at: new Date(),
|
|
110
167
|
},
|
|
111
168
|
});
|
|
112
|
-
|
|
169
|
+
sendFastSuccess(res, 201, permissionSerializer, {
|
|
170
|
+
status: "success",
|
|
171
|
+
message: "Permission created",
|
|
172
|
+
data: { ...permission, id: permission.id.toString() },
|
|
173
|
+
});
|
|
113
174
|
}
|
|
114
175
|
|
|
115
176
|
export async function listPermissions(_req: Request, res: Response) {
|
|
116
177
|
const permissions = await prisma.permissions.findMany({
|
|
117
178
|
orderBy: { id: "asc" },
|
|
118
179
|
});
|
|
119
|
-
|
|
180
|
+
const serialized = permissions.map((p: any) => ({
|
|
181
|
+
...p,
|
|
182
|
+
id: p.id.toString(),
|
|
183
|
+
}));
|
|
184
|
+
sendFastSuccess(res, 200, permissionListSerializer, {
|
|
185
|
+
status: "success",
|
|
186
|
+
message: "Permissions list",
|
|
187
|
+
data: serialized,
|
|
188
|
+
});
|
|
120
189
|
}
|
|
121
190
|
|
|
122
191
|
export async function updatePermission(req: Request, res: Response) {
|
|
@@ -152,7 +221,11 @@ export async function updatePermission(req: Request, res: Response) {
|
|
|
152
221
|
updated_at: new Date(),
|
|
153
222
|
},
|
|
154
223
|
});
|
|
155
|
-
|
|
224
|
+
sendFastSuccess(res, 200, permissionSerializer, {
|
|
225
|
+
status: "success",
|
|
226
|
+
message: "Permission updated",
|
|
227
|
+
data: { ...updated, id: updated.id.toString() },
|
|
228
|
+
});
|
|
156
229
|
}
|
|
157
230
|
|
|
158
231
|
export async function deletePermission(req: Request, res: Response) {
|
package/src/core/redis.ts
CHANGED
|
@@ -31,7 +31,7 @@ redis.on("ready", () => {
|
|
|
31
31
|
// console.log("Redis connected!");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
redis.on("error", (
|
|
34
|
+
redis.on("error", (_err) => {
|
|
35
35
|
// If connection fails and we haven't switched to mock yet
|
|
36
36
|
if (!isRedisConnected && !(redis instanceof RedisMock)) {
|
|
37
37
|
// console.log("Redis connection failed, switching to in-memory mock...");
|
|
@@ -98,9 +98,10 @@ export async function initRedis() {
|
|
|
98
98
|
|
|
99
99
|
// Proxy handler to forward all calls to activeRedis
|
|
100
100
|
const redisProxy = new Proxy({} as Redis, {
|
|
101
|
-
get: (
|
|
102
|
-
//
|
|
103
|
-
|
|
101
|
+
get: (_target, prop) => {
|
|
102
|
+
// If accessing a property on the proxy, forward it to activeRedis
|
|
103
|
+
const value = (activeRedis as any)[prop];
|
|
104
|
+
return value;
|
|
104
105
|
},
|
|
105
106
|
});
|
|
106
107
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fastJson from "fast-json-stringify";
|
|
2
|
+
|
|
3
|
+
// Cache untuk menyimpan fungsi stringify yang sudah dicompile
|
|
4
|
+
// Key: Nama schema/Identifier, Value: Fungsi stringify
|
|
5
|
+
const serializerCache = new Map<string, (doc: any) => string>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Membuat atau mengambil serializer yang sudah dicompile.
|
|
9
|
+
*
|
|
10
|
+
* @param key Identifier unik untuk schema (misal: 'UserResponse', 'ProductList')
|
|
11
|
+
* @param schema JSON Schema definition (Standard JSON Schema)
|
|
12
|
+
* @returns Fungsi yang mengubah object menjadi JSON string dengan sangat cepat
|
|
13
|
+
*/
|
|
14
|
+
export function getSerializer(key: string, schema: any) {
|
|
15
|
+
if (serializerCache.has(key)) {
|
|
16
|
+
return serializerCache.get(key)!;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stringify = fastJson(schema);
|
|
20
|
+
serializerCache.set(key, stringify);
|
|
21
|
+
return stringify;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper untuk mendefinisikan schema standar response Lapeh
|
|
26
|
+
* { status: "success", message: string, data: T }
|
|
27
|
+
*/
|
|
28
|
+
export function createResponseSchema(dataSchema: any) {
|
|
29
|
+
return {
|
|
30
|
+
title: "StandardResponse",
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
status: { type: "string" },
|
|
34
|
+
message: { type: "string" },
|
|
35
|
+
data: dataSchema,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper khusus untuk response paginasi
|
|
42
|
+
* { status: "success", message: string, data: { data: T[], meta: ... } }
|
|
43
|
+
*/
|
|
44
|
+
export function createPaginatedResponseSchema(itemSchema: any) {
|
|
45
|
+
return createResponseSchema({
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
data: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: itemSchema,
|
|
51
|
+
},
|
|
52
|
+
meta: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
page: { type: "integer" },
|
|
56
|
+
perPage: { type: "integer" },
|
|
57
|
+
total: { type: "integer" },
|
|
58
|
+
lastPage: { type: "integer" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
package/src/middleware/auth.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
import jwt from "jsonwebtoken";
|
|
3
|
-
import { prisma } from "../core/database";
|
|
4
3
|
import { sendError } from "../utils/response";
|
|
5
4
|
import { ACCESS_TOKEN_EXPIRES_IN_SECONDS } from "../controllers/authController";
|
|
6
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import rateLimit from "express-rate-limit";
|
|
2
|
-
import { redis } from "../core/redis"; // Optional: Use Redis for distributed rate limiting
|
|
2
|
+
// import { redis } from "../core/redis"; // Optional: Use Redis for distributed rate limiting
|
|
3
3
|
|
|
4
4
|
// Rate limiting untuk mencegah brute force dan DDoS ringan
|
|
5
5
|
export const apiLimiter = rateLimit({
|
package/src/routes/auth.ts
CHANGED
|
@@ -30,14 +30,14 @@ if (!fs.existsSync(avatarUploadDir)) {
|
|
|
30
30
|
|
|
31
31
|
const storage = (multer as any).diskStorage({
|
|
32
32
|
destination(
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
_req: any,
|
|
34
|
+
_file: any,
|
|
35
35
|
cb: (error: Error | null, destination: string) => void
|
|
36
36
|
) {
|
|
37
37
|
cb(null, avatarUploadDir);
|
|
38
38
|
},
|
|
39
39
|
filename(
|
|
40
|
-
|
|
40
|
+
_req: any,
|
|
41
41
|
file: any,
|
|
42
42
|
cb: (error: Error | null, filename: string) => void
|
|
43
43
|
) {
|
package/src/utils/response.ts
CHANGED
|
@@ -46,6 +46,27 @@ export function sendSuccess<T>(
|
|
|
46
46
|
return res.status(statusCode).json(toJsonSafe(body));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Mengirim response sukses dengan performa tinggi menggunakan Schema Serialization (Fastify-style).
|
|
51
|
+
* Melewati proses JSON.stringify standar yang lambat.
|
|
52
|
+
*
|
|
53
|
+
* @param serializer Fungsi serializer yang sudah dicompile dari src/core/serializer
|
|
54
|
+
*/
|
|
55
|
+
export function sendFastSuccess(
|
|
56
|
+
res: Response,
|
|
57
|
+
statusCode: number,
|
|
58
|
+
serializer: (doc: any) => string,
|
|
59
|
+
data: any
|
|
60
|
+
) {
|
|
61
|
+
// Set header manual karena kita mengirim raw string
|
|
62
|
+
res.setHeader("Content-Type", "application/json");
|
|
63
|
+
res.status(statusCode);
|
|
64
|
+
|
|
65
|
+
// Serializer mengembalikan string JSON
|
|
66
|
+
const jsonString = serializer(data);
|
|
67
|
+
return res.send(jsonString);
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
export function sendError<T = unknown>(
|
|
50
71
|
res: Response,
|
|
51
72
|
statusCode: number,
|
package/tsconfig.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"module": "CommonJS",
|
|
@@ -9,15 +9,20 @@
|
|
|
9
9
|
"strict": true,
|
|
10
10
|
"esModuleInterop": true,
|
|
11
11
|
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
}
|
|
13
18
|
},
|
|
14
|
-
"include": [
|
|
15
|
-
"src",
|
|
16
|
-
"prisma",
|
|
17
|
-
"generated"
|
|
18
|
-
],
|
|
19
|
-
"exclude": [
|
|
20
|
-
"node_modules",
|
|
21
|
-
"dist"
|
|
22
|
-
]
|
|
23
|
-
}
|
|
19
|
+
"include": [
|
|
20
|
+
"src",
|
|
21
|
+
"prisma",
|
|
22
|
+
"generated"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist"
|
|
27
|
+
]
|
|
28
|
+
}
|