lapeh 2.6.17 → 3.0.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/.env.example +1 -6
- package/README.md +19 -85
- package/bin/index.js +84 -180
- package/dist/lib/bootstrap.d.ts.map +1 -1
- package/dist/lib/bootstrap.js +17 -16
- package/dist/lib/core/store.d.ts +55 -0
- package/dist/lib/core/store.d.ts.map +1 -0
- package/dist/lib/core/store.js +66 -0
- package/dist/lib/middleware/error.d.ts.map +1 -1
- package/dist/lib/middleware/error.js +1 -20
- package/dist/lib/utils/validator.d.ts.map +1 -1
- package/dist/lib/utils/validator.js +3 -32
- package/dist/src/modules/Auth/auth.controller.d.ts.map +1 -1
- package/dist/src/modules/Auth/auth.controller.js +118 -105
- package/dist/src/modules/Rbac/rbac.controller.d.ts.map +1 -1
- package/dist/src/modules/Rbac/rbac.controller.js +141 -140
- package/dist/src/routes/index.d.ts.map +1 -1
- package/dist/src/routes/index.js +0 -5
- package/doc/en/CHEATSHEET.md +3 -7
- package/doc/en/CLI.md +16 -41
- package/doc/en/DEPLOYMENT.md +171 -245
- package/doc/en/GETTING_STARTED.md +1 -25
- package/doc/en/PACKAGES.md +2 -3
- package/doc/en/STRUCTURE.md +1 -11
- package/doc/en/TUTORIAL.md +61 -119
- package/doc/id/CHANGELOG.md +16 -0
- package/doc/id/CHEATSHEET.md +0 -4
- package/doc/id/CLI.md +19 -54
- package/doc/id/DEPLOYMENT.md +171 -245
- package/doc/id/GETTING_STARTED.md +91 -115
- package/doc/id/PACKAGES.md +0 -1
- package/doc/id/STRUCTURE.md +1 -11
- package/doc/id/TUTORIAL.md +51 -109
- package/gitignore.template +0 -10
- package/lib/bootstrap.ts +39 -38
- package/lib/core/store.ts +116 -0
- package/lib/middleware/error.ts +1 -21
- package/lib/utils/validator.ts +3 -39
- package/package.json +4 -18
- package/scripts/init-project.js +2 -108
- package/scripts/make-module.js +1 -12
- package/scripts/seed-json.js +158 -0
- package/src/modules/Auth/auth.controller.ts +156 -106
- package/src/modules/Rbac/rbac.controller.ts +193 -138
- package/src/routes/index.ts +0 -3
- package/src/routes/rbac.ts +42 -42
- package/storage/logs/.0337f5062fe676994d1dc340156e089444e3d6e0-audit.json +5 -10
- package/storage/logs/lapeh-2025-12-30.log +1093 -0
- package/tsconfig.build.json +1 -3
- package/tsconfig.json +0 -1
- package/lib/core/database.ts +0 -5
- package/prisma/base.prisma.template +0 -8
- package/prisma/migrations/20251225163737_init/migration.sql +0 -236
- package/prisma/migrations/20251226000329_create_pets_table/migration.sql +0 -11
- package/prisma/migrations/20251226001249_create_pets_table/migration.sql +0 -82
- package/prisma/migrations/20251226001717_restore_core_models/migration.sql +0 -236
- package/prisma/migrations/migration_lock.toml +0 -3
- package/prisma/schema.prisma +0 -197
- package/prisma/seed.ts +0 -411
- package/scripts/compile-schema.js +0 -64
- package/src/modules/Auth/auth.prisma +0 -106
- package/src/modules/Pets/pets.controller.ts +0 -238
- package/src/modules/Pets/pets.prisma +0 -9
- package/src/modules/Rbac/rbac.prisma +0 -68
- package/src/routes/pets.ts +0 -13
- package/storage/logs/lapeh-2025-12-26.log +0 -88
- package/storage/logs/lapeh-2025-12-27.log +0 -217
package/doc/id/STRUCTURE.md
CHANGED
|
@@ -9,7 +9,6 @@ Untuk memahami Lapeh Framework sepenuhnya, Anda perlu tahu apa fungsi setiap fil
|
|
|
9
9
|
| `bin/` | Berisi script eksekusi untuk CLI (`npx lapeh`). Anda jarang menyentuh ini. |
|
|
10
10
|
| `doc/` | Dokumentasi proyek ini berada. |
|
|
11
11
|
| `lib/` | **Framework Core**. Bagian internal framework yang jarang Anda sentuh. |
|
|
12
|
-
| `prisma/` | Jantung konfigurasi Database. |
|
|
13
12
|
| `scripts/` | Kumpulan script Node.js untuk utility (generator, compiler schema, dll). |
|
|
14
13
|
| `src/` | **Source Code Utama**. 99% kodingan Anda ada di sini. |
|
|
15
14
|
| `.env` | Variabel rahasia (Database URL, API Keys). **Jangan commit file ini ke Git!** |
|
|
@@ -29,7 +28,6 @@ Lapeh menggunakan pendekatan **Modular**. Setiap fitur dikelompokkan dalam satu
|
|
|
29
28
|
Contoh struktur modul `Auth`:
|
|
30
29
|
|
|
31
30
|
- `Auth/auth.controller.ts`: Logika aplikasi (Controller).
|
|
32
|
-
- `Auth/auth.prisma`: Definisi tabel database (Model).
|
|
33
31
|
|
|
34
32
|
### `src/routes/`
|
|
35
33
|
|
|
@@ -54,7 +52,6 @@ Bagian ini mirip dengan `node_modules` atau folder `.next` di Next.js. Ini adala
|
|
|
54
52
|
Bagian "Mesin" framework.
|
|
55
53
|
|
|
56
54
|
- `server.ts`: Setup Express App.
|
|
57
|
-
- `database.ts`: Instance Prisma Client.
|
|
58
55
|
- `redis.ts`: Koneksi Redis.
|
|
59
56
|
- `serializer.ts`: Logic caching JSON Schema.
|
|
60
57
|
|
|
@@ -74,18 +71,11 @@ Fungsi bantuan (Helper) bawaan.
|
|
|
74
71
|
- `response.ts`: Standar format JSON response (`sendFastSuccess`, `sendError`).
|
|
75
72
|
- `logger.ts`: Sistem logging (Winston).
|
|
76
73
|
|
|
77
|
-
## Folder `prisma/`
|
|
78
|
-
|
|
79
|
-
- `migrations/`: History perubahan database (SQL file). Jangan diedit manual.
|
|
80
|
-
- `base.prisma.template`: Header dari schema database (berisi konfigurasi datasource db).
|
|
81
|
-
- `seed.ts`: Script untuk mengisi data awal (Data Seeding).
|
|
82
|
-
|
|
83
74
|
## Folder `scripts/`
|
|
84
75
|
|
|
85
76
|
Script-script "Magic" yang dijalankan `npm run`.
|
|
86
77
|
|
|
87
|
-
- `make-module.js`: Generator modul baru (Controller
|
|
88
|
-
- `compile-schema.js`: Penggabung file `.prisma` dari setiap modul menjadi satu `schema.prisma`.
|
|
78
|
+
- `make-module.js`: Generator modul baru (Controller).
|
|
89
79
|
- `init-project.js`: Wizard setup awal.
|
|
90
80
|
- `generate-jwt-secret.js`: Generator kunci rahasia JWT otomatis.
|
|
91
81
|
|
package/doc/id/TUTORIAL.md
CHANGED
|
@@ -4,44 +4,12 @@ Dalam tutorial ini, kita akan membangun fitur "Manajemen Buku" sederhana menggun
|
|
|
4
4
|
1. **CLI** untuk generate kode.
|
|
5
5
|
2. **Validator** untuk validasi input.
|
|
6
6
|
3. **Fast Serialization** untuk respon cepat.
|
|
7
|
-
4. **RBAC** untuk proteksi delete (Admin only).
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
> **Catatan**: Tutorial ini menggunakan array in-memory untuk penyimpanan data agar tetap sederhana. Lapeh v3.0.0 bersifat database-agnostic, jadi Anda bebas menggantinya dengan Prisma, TypeORM, atau library database lainnya.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
## Langkah 1: Generate Module (Controller & Route)
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
npm run make:model Book
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
File baru akan muncul di `src/models/book.prisma`. Edit file tersebut:
|
|
18
|
-
|
|
19
|
-
```prisma
|
|
20
|
-
// src/models/book.prisma
|
|
21
|
-
|
|
22
|
-
model Book {
|
|
23
|
-
id BigInt @id @default(autoincrement())
|
|
24
|
-
title String
|
|
25
|
-
author String
|
|
26
|
-
isbn String @unique
|
|
27
|
-
publishedAt DateTime
|
|
28
|
-
stock Int @default(0)
|
|
29
|
-
created_at DateTime @default(now())
|
|
30
|
-
updated_at DateTime @updatedAt
|
|
31
|
-
|
|
32
|
-
@@map("books")
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Terapkan perubahan ke database:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm run prisma:migrate
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Langkah 2: Generate Module (Controller & Route)
|
|
43
|
-
|
|
44
|
-
Kita buat controller dan route sekaligus.
|
|
12
|
+
Kita akan membuat controller dan route untuk fitur Buku.
|
|
45
13
|
|
|
46
14
|
```bash
|
|
47
15
|
npm run make:module Book
|
|
@@ -51,24 +19,33 @@ Framework akan membuat:
|
|
|
51
19
|
- `src/controllers/bookController.ts`
|
|
52
20
|
- `src/routes/book.ts`
|
|
53
21
|
|
|
54
|
-
## Langkah
|
|
22
|
+
## Langkah 2: Implementasi Controller
|
|
55
23
|
|
|
56
|
-
Buka `src/controllers/bookController.ts` dan kita implementasikan fitur **Create** dan **List
|
|
24
|
+
Buka `src/controllers/bookController.ts` dan kita implementasikan fitur **Create** dan **List**.
|
|
57
25
|
|
|
58
|
-
### Setup Import &
|
|
26
|
+
### Setup Import & Data Store
|
|
59
27
|
|
|
60
28
|
```typescript
|
|
61
29
|
import { Request, Response } from "express";
|
|
62
|
-
import { prisma } from "@/core/database";
|
|
63
30
|
import { sendFastSuccess, sendError } from "@/utils/response";
|
|
64
31
|
import { Validator } from "@/utils/validator";
|
|
65
32
|
import { getSerializer, createResponseSchema } from "@/core/serializer";
|
|
66
33
|
|
|
34
|
+
// Simpan data di memory (Array sederhana)
|
|
35
|
+
interface Book {
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
author: string;
|
|
39
|
+
isbn: string;
|
|
40
|
+
stock: number;
|
|
41
|
+
}
|
|
42
|
+
const books: Book[] = [];
|
|
43
|
+
|
|
67
44
|
// 1. Definisikan Schema Output (untuk Fastify Serialization)
|
|
68
45
|
const bookSchema = {
|
|
69
46
|
type: "object",
|
|
70
47
|
properties: {
|
|
71
|
-
id: { type: "string" },
|
|
48
|
+
id: { type: "string" },
|
|
72
49
|
title: { type: "string" },
|
|
73
50
|
author: { type: "string" },
|
|
74
51
|
isbn: { type: "string" },
|
|
@@ -92,101 +69,66 @@ export async function createBook(req: Request, res: Response) {
|
|
|
92
69
|
const validator = await Validator.make(req.body, {
|
|
93
70
|
title: "required|string|min:3",
|
|
94
71
|
author: "required|string",
|
|
95
|
-
isbn: "required|string
|
|
96
|
-
stock: "required|number|min:1"
|
|
97
|
-
publishedAt: "required|string" // Format tanggal ISO
|
|
72
|
+
isbn: "required|string",
|
|
73
|
+
stock: "required|number|min:1"
|
|
98
74
|
});
|
|
99
75
|
|
|
100
76
|
if (validator.fails()) {
|
|
101
77
|
return sendError(res, 400, "Validation Error", validator.errors());
|
|
102
78
|
}
|
|
103
79
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
author: data.author,
|
|
111
|
-
isbn: data.isbn,
|
|
112
|
-
stock: data.stock,
|
|
113
|
-
publishedAt: new Date(data.publishedAt)
|
|
114
|
-
}
|
|
115
|
-
});
|
|
80
|
+
// 2. Simpan Data (In-Memory)
|
|
81
|
+
const newBook: Book = {
|
|
82
|
+
id: Date.now().toString(),
|
|
83
|
+
...validator.validated()
|
|
84
|
+
};
|
|
85
|
+
books.push(newBook);
|
|
116
86
|
|
|
117
|
-
// 3.
|
|
118
|
-
return sendFastSuccess(res,
|
|
119
|
-
status: "success",
|
|
120
|
-
message: "Buku berhasil ditambahkan",
|
|
121
|
-
data: { ...book, id: book.id.toString() } // Konversi BigInt manual jika perlu
|
|
122
|
-
});
|
|
87
|
+
// 3. Kirim Response (Serialized)
|
|
88
|
+
return sendFastSuccess(res, bookDetailSerializer(newBook), 201);
|
|
123
89
|
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Implementasi List (High Performance)
|
|
127
90
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
take: 50, // Limit 50
|
|
132
|
-
orderBy: { created_at: "desc" }
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Convert BigInt to string sebelum passing ke serializer (opsional, tapi aman)
|
|
136
|
-
const safeBooks = books.map(b => ({ ...b, id: b.id.toString() }));
|
|
137
|
-
|
|
138
|
-
return sendFastSuccess(res, 200, bookListSerializer, {
|
|
139
|
-
status: "success",
|
|
140
|
-
message: "Daftar buku",
|
|
141
|
-
data: safeBooks
|
|
142
|
-
});
|
|
91
|
+
export async function listBooks(req: Request, res: Response) {
|
|
92
|
+
// Return semua buku
|
|
93
|
+
return sendFastSuccess(res, bookListSerializer(books));
|
|
143
94
|
}
|
|
144
95
|
```
|
|
145
96
|
|
|
146
|
-
## Langkah
|
|
97
|
+
## Langkah 3: Register Route
|
|
147
98
|
|
|
148
|
-
Buka `src/routes/book.ts
|
|
99
|
+
Buka `src/routes/book.ts`. CLI sudah membuat struktur dasarnya. Kita hanya perlu menghubungkannya dengan fungsi controller kita.
|
|
149
100
|
|
|
150
101
|
```typescript
|
|
151
102
|
import { Router } from "express";
|
|
152
|
-
import { createBook,
|
|
153
|
-
import { requireAuth, requireAdmin } from "../middleware/auth";
|
|
103
|
+
import { createBook, listBooks } from "../controllers/bookController";
|
|
154
104
|
|
|
155
|
-
|
|
105
|
+
const router = Router();
|
|
156
106
|
|
|
157
|
-
|
|
158
|
-
|
|
107
|
+
router.post("/", createBook);
|
|
108
|
+
router.get("/", listBooks);
|
|
159
109
|
|
|
160
|
-
|
|
161
|
-
bookRouter.post("/", requireAuth, requireAdmin, createBook);
|
|
110
|
+
export default router;
|
|
162
111
|
```
|
|
163
112
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import { bookRouter } from "./book";
|
|
168
|
-
// ...
|
|
169
|
-
router.use("/books", bookRouter);
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Langkah 5: Testing
|
|
113
|
+
## Langkah 4: Test API Anda
|
|
173
114
|
|
|
174
115
|
Jalankan server:
|
|
175
116
|
```bash
|
|
176
117
|
npm run dev
|
|
177
118
|
```
|
|
178
119
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
120
|
+
Test dengan curl atau Postman:
|
|
121
|
+
|
|
122
|
+
**Buat Buku Baru:**
|
|
123
|
+
```bash
|
|
124
|
+
curl -X POST http://localhost:4000/api/book \
|
|
125
|
+
-H "Content-Type: application/json" \
|
|
126
|
+
-d '{"title":"Panduan Lapeh", "author":"Tim Lapeh", "isbn":"12345", "stock":10}'
|
|
127
|
+
```
|
|
185
128
|
|
|
186
|
-
|
|
129
|
+
**List Buku:**
|
|
130
|
+
```bash
|
|
131
|
+
curl http://localhost:4000/api/book
|
|
132
|
+
```
|
|
187
133
|
|
|
188
|
-
|
|
189
|
-
- **Aman** (Validasi, Auth, RBAC).
|
|
190
|
-
- **Cepat** (Fast Serialization).
|
|
191
|
-
- **Rapi** (Struktur terstandarisasi).
|
|
192
|
-
- **Mudah** (CLI Generator).
|
|
134
|
+
Selamat! Anda telah membangun API yang cepat dan tervalidasi tanpa terjebak dalam konfigurasi database yang rumit.
|
package/gitignore.template
CHANGED
package/lib/bootstrap.ts
CHANGED
|
@@ -10,7 +10,6 @@ import http from "http";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { initRealtime } from "./core/realtime";
|
|
12
12
|
import { initRedis, redis } from "./core/redis";
|
|
13
|
-
import { prisma } from "./core/database";
|
|
14
13
|
import { visitorCounter } from "./middleware/visitor";
|
|
15
14
|
import { errorHandler } from "./middleware/error";
|
|
16
15
|
import { apiLimiter } from "./middleware/rateLimit";
|
|
@@ -23,31 +22,41 @@ export async function createApp() {
|
|
|
23
22
|
// We map '@lapeh' to the directory containing this file (lib/ or dist/lib/)
|
|
24
23
|
moduleAlias.addAlias("@lapeh", __dirname);
|
|
25
24
|
|
|
26
|
-
//
|
|
25
|
+
// Register alias for src directory (@/) to support imports in controllers/routes
|
|
27
26
|
const isProduction = process.env.NODE_ENV === "production";
|
|
27
|
+
moduleAlias.addAlias(
|
|
28
|
+
"@",
|
|
29
|
+
isProduction
|
|
30
|
+
? path.join(process.cwd(), "dist", "src")
|
|
31
|
+
: path.join(process.cwd(), "src")
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// LOAD USER CONFIG
|
|
28
35
|
const configPath = isProduction
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
? path.join(process.cwd(), "dist", "src", "config")
|
|
37
|
+
: path.join(process.cwd(), "src", "config");
|
|
31
38
|
|
|
32
39
|
let appConfig: any = { timeout: 30000, jsonLimit: "10mb" };
|
|
33
|
-
let corsConfig: any = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
let corsConfig: any = {
|
|
41
|
+
origin: process.env.CORS_ORIGIN || "*",
|
|
42
|
+
credentials: true,
|
|
43
|
+
exposedHeaders: ["x-access-token", "x-access-expires-at"],
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
const appConfModule = require(path.join(configPath, "app"));
|
|
48
|
+
if (appConfModule.appConfig)
|
|
49
|
+
appConfig = { ...appConfig, ...appConfModule.appConfig };
|
|
42
50
|
} catch (e) {
|
|
43
|
-
|
|
51
|
+
// ignore
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
const corsConfModule = require(path.join(configPath, "cors"));
|
|
56
|
+
if (corsConfModule.corsConfig)
|
|
57
|
+
corsConfig = { ...corsConfig, ...corsConfModule.corsConfig };
|
|
49
58
|
} catch (e) {
|
|
50
|
-
|
|
59
|
+
// ignore
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
const app = express();
|
|
@@ -61,7 +70,7 @@ export async function createApp() {
|
|
|
61
70
|
res.setTimeout(timeout, () => {
|
|
62
71
|
res.status(408).send({
|
|
63
72
|
status: "error",
|
|
64
|
-
message: `Request Timeout (${timeout/1000}s limit)`,
|
|
73
|
+
message: `Request Timeout (${timeout / 1000}s limit)`,
|
|
65
74
|
});
|
|
66
75
|
});
|
|
67
76
|
next();
|
|
@@ -78,7 +87,9 @@ export async function createApp() {
|
|
|
78
87
|
|
|
79
88
|
app.use(requestLogger);
|
|
80
89
|
app.use(express.json({ limit: appConfig.jsonLimit || "10mb" }));
|
|
81
|
-
app.use(
|
|
90
|
+
app.use(
|
|
91
|
+
express.urlencoded({ extended: true, limit: appConfig.jsonLimit || "10mb" })
|
|
92
|
+
);
|
|
82
93
|
app.use(apiLimiter);
|
|
83
94
|
app.use(visitorCounter);
|
|
84
95
|
|
|
@@ -93,35 +104,24 @@ export async function createApp() {
|
|
|
93
104
|
|
|
94
105
|
// DYNAMIC ROUTE LOADING
|
|
95
106
|
try {
|
|
107
|
+
console.log("BOOTSTRAP: Loading routes. NODE_ENV=", process.env.NODE_ENV);
|
|
96
108
|
const isProduction = process.env.NODE_ENV === "production";
|
|
97
|
-
|
|
109
|
+
let userRoutesPath = isProduction
|
|
98
110
|
? path.join(process.cwd(), "dist", "src", "routes")
|
|
99
111
|
: path.join(process.cwd(), "src", "routes");
|
|
100
112
|
|
|
113
|
+
// In test environment, explicitly point to index to ensure resolution
|
|
114
|
+
if (process.env.NODE_ENV === "test") {
|
|
115
|
+
// In test environment (ts-jest), we need to point to the TS file
|
|
116
|
+
// And we might need to use the full path with extension
|
|
117
|
+
userRoutesPath = path.join(process.cwd(), "src", "routes", "index.ts");
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
// Gunakan require agar sinkron dan mudah dicatch
|
|
102
121
|
// Check if file exists before requiring to avoid crash in tests/clean env
|
|
103
122
|
try {
|
|
104
|
-
// In test environment, we might need to point to src/routes explicitly if not compiled
|
|
105
|
-
// const routesPath = process.env.NODE_ENV === 'test'
|
|
106
|
-
// ? path.join(process.cwd(), "src", "routes", "index.ts")
|
|
107
|
-
// : userRoutesPath;
|
|
108
|
-
|
|
109
|
-
// Note: For TS files in jest, we rely on ts-jest handling 'require' if it points to .ts or we need to use 'import'
|
|
110
|
-
// But 'require' in jest with ts-jest should work if configured.
|
|
111
|
-
|
|
112
|
-
// However, require(path) with .ts extension might be tricky.
|
|
113
|
-
// Let's stick to userRoutesPath but maybe adjust for test env.
|
|
114
|
-
|
|
115
|
-
// Check if we are in test environment and using ts-jest
|
|
116
|
-
// If so, we might need to import the TS file directly via relative path if alias is not working for require
|
|
117
|
-
|
|
118
123
|
const { apiRouter } = require(userRoutesPath);
|
|
119
124
|
app.use("/api", apiRouter);
|
|
120
|
-
console.log(
|
|
121
|
-
`✅ User routes loaded successfully from ${
|
|
122
|
-
isProduction ? "dist/" : ""
|
|
123
|
-
}src/routes`
|
|
124
|
-
);
|
|
125
125
|
} catch (e) {
|
|
126
126
|
// If it's just missing module, maybe we are in test mode or fresh install
|
|
127
127
|
if (process.env.NODE_ENV !== "test") {
|
|
@@ -134,10 +134,12 @@ export async function createApp() {
|
|
|
134
134
|
`Error loading routes in test mode from ${userRoutesPath}:`,
|
|
135
135
|
e
|
|
136
136
|
);
|
|
137
|
+
throw e;
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
} catch (error) {
|
|
140
141
|
console.error(error);
|
|
142
|
+
if (process.env.NODE_ENV === "test") throw error;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
app.use(errorHandler);
|
|
@@ -147,7 +149,7 @@ export async function createApp() {
|
|
|
147
149
|
|
|
148
150
|
export async function bootstrap() {
|
|
149
151
|
// Validasi Environment Variables
|
|
150
|
-
const requiredEnvs = ["
|
|
152
|
+
const requiredEnvs = ["JWT_SECRET"];
|
|
151
153
|
const missingEnvs = requiredEnvs.filter((key) => !process.env[key]);
|
|
152
154
|
if (missingEnvs.length > 0) {
|
|
153
155
|
console.error(
|
|
@@ -186,7 +188,6 @@ export async function bootstrap() {
|
|
|
186
188
|
console.log(`\n🛑 ${signal} received. Closing resources...`);
|
|
187
189
|
server.close(() => console.log("Http server closed."));
|
|
188
190
|
try {
|
|
189
|
-
await prisma.$disconnect();
|
|
190
191
|
if (redis && redis.status === "ready") await redis.quit();
|
|
191
192
|
process.exit(0);
|
|
192
193
|
} catch (err) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export interface User {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
uuid: string;
|
|
10
|
+
avatar?: string | null;
|
|
11
|
+
avatar_url?: string | null;
|
|
12
|
+
email_verified_at?: string | Date | null;
|
|
13
|
+
created_at: string | Date;
|
|
14
|
+
updated_at: string | Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Role {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
description?: string | null;
|
|
22
|
+
created_at: string | Date;
|
|
23
|
+
updated_at: string | Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Permission {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
description?: string | null;
|
|
31
|
+
created_at: string | Date;
|
|
32
|
+
updated_at: string | Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UserRole {
|
|
36
|
+
id: string;
|
|
37
|
+
user_id: string;
|
|
38
|
+
role_id: string;
|
|
39
|
+
created_at: string | Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RolePermission {
|
|
43
|
+
id: string;
|
|
44
|
+
role_id: string;
|
|
45
|
+
permission_id: string;
|
|
46
|
+
created_at: string | Date;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UserPermission {
|
|
50
|
+
id: string;
|
|
51
|
+
user_id: string;
|
|
52
|
+
permission_id: string;
|
|
53
|
+
created_at: string | Date;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Database file path
|
|
57
|
+
const dbPath = path.resolve(process.cwd(), "database.json");
|
|
58
|
+
|
|
59
|
+
// Load data function
|
|
60
|
+
function loadData() {
|
|
61
|
+
if (fs.existsSync(dbPath)) {
|
|
62
|
+
const raw = fs.readFileSync(dbPath, "utf-8");
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
users: [],
|
|
67
|
+
roles: [
|
|
68
|
+
{
|
|
69
|
+
id: "1",
|
|
70
|
+
name: "Admin",
|
|
71
|
+
slug: "admin",
|
|
72
|
+
description: "Administrator",
|
|
73
|
+
created_at: new Date(),
|
|
74
|
+
updated_at: new Date(),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "2",
|
|
78
|
+
name: "User",
|
|
79
|
+
slug: "user",
|
|
80
|
+
description: "Standard User",
|
|
81
|
+
created_at: new Date(),
|
|
82
|
+
updated_at: new Date(),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
permissions: [],
|
|
86
|
+
user_roles: [],
|
|
87
|
+
role_permissions: [],
|
|
88
|
+
user_permissions: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = loadData();
|
|
93
|
+
|
|
94
|
+
// Export mutable arrays
|
|
95
|
+
export const users: User[] = data.users;
|
|
96
|
+
export const roles: Role[] = data.roles;
|
|
97
|
+
export const permissions: Permission[] = data.permissions;
|
|
98
|
+
export const user_roles: UserRole[] = data.user_roles;
|
|
99
|
+
export const role_permissions: RolePermission[] = data.role_permissions;
|
|
100
|
+
export const user_permissions: UserPermission[] = data.user_permissions;
|
|
101
|
+
|
|
102
|
+
// Helper to save data
|
|
103
|
+
export function saveStore() {
|
|
104
|
+
const payload = {
|
|
105
|
+
users,
|
|
106
|
+
roles,
|
|
107
|
+
permissions,
|
|
108
|
+
user_roles,
|
|
109
|
+
role_permissions,
|
|
110
|
+
user_permissions,
|
|
111
|
+
};
|
|
112
|
+
fs.writeFileSync(dbPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Helper to generate IDs
|
|
116
|
+
export const generateId = () => Math.random().toString(36).substr(2, 9);
|
package/lib/middleware/error.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
import { ZodError } from "zod";
|
|
3
|
-
import { Prisma } from "@prisma/client";
|
|
4
3
|
import { sendError } from "../utils/response";
|
|
5
4
|
import { Log } from "../utils/logger";
|
|
6
5
|
|
|
@@ -19,26 +18,7 @@ export function errorHandler(
|
|
|
19
18
|
return sendError(res, 400, "Validation Error", formattedErrors);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
// 2.
|
|
23
|
-
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
24
|
-
// P2002: Unique constraint failed
|
|
25
|
-
if (err.code === "P2002") {
|
|
26
|
-
const target = (err.meta?.target as string[]) || [];
|
|
27
|
-
const fields = target.length > 0 ? target.join(", ") : "field";
|
|
28
|
-
return sendError(res, 409, `Unique constraint failed on: ${fields}`);
|
|
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
|
-
}
|
|
35
|
-
// P2025: Record not found
|
|
36
|
-
if (err.code === "P2025") {
|
|
37
|
-
return sendError(res, 404, "Record not found");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 3. JWT Errors
|
|
21
|
+
// 2. JWT Errors
|
|
42
22
|
if (err.name === "JsonWebTokenError") {
|
|
43
23
|
return sendError(res, 401, "Invalid token");
|
|
44
24
|
}
|
package/lib/utils/validator.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z, ZodSchema, ZodError, ZodIssue } from "zod";
|
|
2
|
-
import { prisma } from "../core/database";
|
|
3
2
|
|
|
4
3
|
export class Validator {
|
|
5
4
|
private data: any;
|
|
@@ -242,44 +241,9 @@ export class Validator {
|
|
|
242
241
|
break;
|
|
243
242
|
case "unique":
|
|
244
243
|
// unique:table,column,ignore,idColumn
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
async (val: any) => {
|
|
249
|
-
if (!val) return true;
|
|
250
|
-
const where: any = { [column]: val };
|
|
251
|
-
if (ignoreValue && ignoreValue !== "null") {
|
|
252
|
-
// Try to handle numeric IDs if ignoreValue looks numeric
|
|
253
|
-
const ignoreVal = !isNaN(Number(ignoreValue))
|
|
254
|
-
? Number(ignoreValue)
|
|
255
|
-
: ignoreValue;
|
|
256
|
-
// But Prisma uses BigInt for IDs often in this project?
|
|
257
|
-
// Let's assume string or number is fine, user can cast if needed.
|
|
258
|
-
// In this project, IDs are BigInt.
|
|
259
|
-
if (
|
|
260
|
-
typeof ignoreVal === "number" ||
|
|
261
|
-
/^\d+$/.test(String(ignoreValue))
|
|
262
|
-
) {
|
|
263
|
-
where[ignoreColumn] = { not: BigInt(ignoreValue) };
|
|
264
|
-
} else {
|
|
265
|
-
where[ignoreColumn] = { not: ignoreValue };
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
// @ts-ignore
|
|
271
|
-
const count = await prisma[table].count({ where });
|
|
272
|
-
return count === 0;
|
|
273
|
-
} catch (e) {
|
|
274
|
-
console.error(
|
|
275
|
-
`Validator unique check failed for table ${table}:`,
|
|
276
|
-
e
|
|
277
|
-
);
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
{ message: `The ${column} has already been taken.` }
|
|
282
|
-
);
|
|
244
|
+
// NOTE: Unique check requires Database implementation.
|
|
245
|
+
// Since v3.0.0 (No-ORM), this rule is disabled by default.
|
|
246
|
+
// You should implement your own uniqueness check manually in the controller.
|
|
283
247
|
break;
|
|
284
248
|
}
|
|
285
249
|
}
|