lapeh 2.2.1 → 2.2.3

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/readme.md CHANGED
@@ -14,6 +14,46 @@ Cocok untuk developer yang mencari **Express boilerplate** dengan fitur lengkap:
14
14
  - **Smart Caching**: Otomatis menggunakan Redis jika tersedia, fallback ke in-memory jika tidak.
15
15
  - **Secure by Default**: Dilengkapi Helmet, Rate Limiting, CORS, dan JWT Auth.
16
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
+ ## 🔮 Roadmap (Rencana Masa Depan)
21
+
22
+ Lapeh Framework akan terus berkembang menjadi solusi Enterprise yang lengkap. Kami memiliki rencana besar untuk fitur-fitur seperti **Job Queues**, **Storage Abstraction (S3)**, **Mailer**, dan **OpenAPI Generator**.
23
+
24
+ Lihat detail rencana pengembangan di **[ROADMAP.md](doc/ROADMAP.md)**.
25
+
26
+ ## 🤝 Berkontribusi (Open Source)
27
+
28
+ Lapeh adalah proyek Open Source dan kami sangat terbuka untuk kontribusi dari komunitas! Baik itu perbaikan bug, penambahan fitur, atau perbaikan dokumentasi.
29
+
30
+ Ingin ikut berkontribusi? Silakan baca **[Panduan Kontribusi (CONTRIBUTING.md)](doc/CONTRIBUTING.md)** untuk memulai.
31
+
32
+ ## 📚 Dokumentasi Lengkap
33
+
34
+ Kami menyusun "Learning Path" agar Anda bisa memahami framework ini dari nol hingga mahir.
35
+
36
+ ### 🐣 Level 1: Pemula (Wajib Baca)
37
+
38
+ - **[Pengenalan Framework](doc/INTRODUCTION.md)**: Mengapa framework ini ada? Apa bedanya dengan yang lain?
39
+ - **[Getting Started](doc/GETTING_STARTED.md)**: Instalasi dan setup awal.
40
+ - **[Bedah Struktur Folder](doc/STRUCTURE.md)**: Pahami fungsi setiap file dan direktori.
41
+ - **[Referensi Package](doc/PACKAGES.md)**: Penjelasan kegunaan setiap library yang terinstall.
42
+ - **[Cheatsheet (Contekan)](doc/CHEATSHEET.md)**: Daftar perintah & kode cepat.
43
+
44
+ ### 🔨 Level 2: Membangun Aplikasi
45
+
46
+ - **[CLI Tools](doc/CLI.md)**: Percepat kerja dengan generator kode (`make:module`, dll).
47
+ - **[Tutorial Studi Kasus](doc/TUTORIAL.md)**: Bikin API "Perpustakaan" dari nol sampai jadi.
48
+ - **[Fitur & Konsep Inti](doc/FEATURES.md)**: Validasi, Auth, RBAC, dan Serializer.
49
+
50
+ ### 🚀 Level 3: Mahir & Production
51
+
52
+ - **[Performance Guide](doc/PERFORMANCE.md)**: Tips optimasi high-scale app.
53
+ - **[Security Best Practices](doc/SECURITY.md)**: Panduan mengamankan aplikasi.
54
+ - **[Deployment Guide](doc/DEPLOYMENT.md)**: Cara deploy ke VPS, Docker, atau Cloud.
55
+ - **[FAQ & Troubleshooting](doc/FAQ.md)**: Solusi masalah umum.
56
+ - **[Changelog](doc/CHANGELOG.md)**: Riwayat versi.
17
57
 
18
58
  ## 📦 Instalasi & Penggunaan
19
59
 
@@ -251,6 +291,7 @@ MIT
251
291
  ## 🚀 Deployment Guide
252
292
 
253
293
  ### 1) Build & Generate Prisma Client (Otomatis)
294
+
254
295
  - Build: `npm run build`
255
296
  - Start (dev): `npm run start`
256
297
  - Start (prod): `npm run start:prod`
@@ -258,6 +299,7 @@ MIT
258
299
  - `prebuild`, `prestart`, dan `prestart:prod` akan memanggil `npm run prisma:generate` sehingga Prisma Client selalu tersedia tanpa error.
259
300
 
260
301
  ### 2) Production Environment
302
+
261
303
  - Pastikan `.env` berisi kredensial production:
262
304
  - `DATABASE_URL` dan `DATABASE_PROVIDER` (mysql/postgresql)
263
305
  - `JWT_SECRET` (gunakan `npm run generate:jwt` untuk mengganti)
@@ -268,6 +310,7 @@ npm run prisma:deploy
268
310
  ```
269
311
 
270
312
  ### 3) Menjalankan dengan PM2
313
+
271
314
  - Install PM2:
272
315
 
273
316
  ```bash
@@ -296,6 +339,7 @@ pm2 restart lapeh-api
296
339
  ```
297
340
 
298
341
  ### 4) Nginx Reverse Proxy (Recommended)
342
+
299
343
  - Buat server block `/etc/nginx/sites-available/lapeh`:
300
344
 
301
345
  ```nginx
@@ -329,6 +373,7 @@ sudo certbot --nginx -d example.com
329
373
  ```
330
374
 
331
375
  ### 5) Apache 2 Reverse Proxy (Alternatif)
376
+
332
377
  - Enable modul proxy:
333
378
 
334
379
  ```bash
@@ -362,6 +407,7 @@ sudo systemctl reload apache2
362
407
  ```
363
408
 
364
409
  ### 6) Checklist Produksi
410
+
365
411
  - `npm run prisma:deploy` sukses dan tabel terbentuk
366
412
  - `pm2 status` menunjukkan proses hidup
367
413
  - 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 "../core/database";
6
- import { sendSuccess, sendError } from "../utils/response";
7
- import { Validator } from "../utils/validator";
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
- sendSuccess(res, 200, "Registration successful", {
51
- id: user.id.toString(),
52
- email: user.email,
53
- name: user.name,
54
- role: defaultRole ? defaultRole.slug : "user",
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
- sendSuccess(res, 200, "Login successful", {
123
- token,
124
- refreshToken,
125
- expiresIn: accessExpiresInSeconds,
126
- expiresAt: accessExpiresAt,
127
- name: user.name,
128
- role: primaryUserRole,
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
- sendSuccess(res, 200, "User profile", {
154
- ...rest,
155
- id: user.id.toString(),
156
- role:
157
- user.user_roles && user.user_roles.length > 0 && user.user_roles[0].role
158
- ? user.user_roles[0].role.slug
159
- : "user",
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(req: Request, res: Response) {
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
- sendSuccess(res, 200, "Token refreshed", {
221
- token,
222
- expiresIn: accessExpiresInSeconds,
223
- expiresAt: accessExpiresAt,
224
- name: user.name,
225
- role: primaryUserRole,
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
- sendSuccess(res, 200, "Avatar updated successfully", {
272
- ...rest,
273
- id: updated.id.toString(),
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
- sendSuccess(res, 200, "Profile updated successfully", {
347
- ...rest,
348
- id: updated.id.toString(),
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 "../core/database";
3
- import { sendSuccess, sendError } from "../utils/response";
4
- import { getPagination, buildPaginationMeta } from "../utils/pagination";
5
- import { Validator } from "../utils/validator";
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
- sendSuccess(res, 200, "Pets retrieved successfully", {
37
- data: serialized,
38
- meta,
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
- sendSuccess(res, 200, "Pet retrieved successfully", {
54
- ...pet,
55
- id: pet.id.toString(),
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
- sendSuccess(res, 201, "Pet created successfully", {
81
- ...pet,
82
- id: pet.id.toString(),
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
- sendSuccess(res, 200, "Pet updated successfully", {
118
- ...updated,
119
- id: updated.id.toString(),
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 "../core/database";
3
- import { sendSuccess, sendError } from "../utils/response";
4
- import { Validator } from "../utils/validator";
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
- sendSuccess(res, 201, "Role created", role);
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
- sendSuccess(res, 200, "Roles list", roles);
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
- sendSuccess(res, 200, "Role updated", updated);
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
- sendSuccess(res, 201, "Permission created", permission);
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
- sendSuccess(res, 200, "Permissions list", permissions);
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
- sendSuccess(res, 200, "Permission updated", updated);
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", (err) => {
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: (target, prop) => {
102
- // @ts-ignore
103
- return activeRedis[prop];
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
+ }
@@ -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({