lapeh 1.0.8 → 1.0.10
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 +10 -3
- package/bin/index.js +4 -4
- package/document.md +205 -0
- package/framework.md +1 -1
- package/package.json +2 -1
- package/prisma/schema.prisma +1 -1
- package/readme.md +21 -4
- package/scripts/check-update.js +3 -0
- package/scripts/compile-schema.js +1 -1
- package/scripts/init-project.js +4 -4
- package/src/index.ts +3 -13
- package/src/middleware/error.ts +57 -7
- package/src/middleware/visitor.ts +3 -5
- package/src/redis.ts +121 -69
- /package/prisma/{base.prisma → base.prisma.template} +0 -0
package/.env.example
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
PORT=4000
|
|
2
2
|
DATABASE_PROVIDER="postgresql"
|
|
3
|
-
DATABASE_URL="postgresql://
|
|
3
|
+
DATABASE_URL="postgresql://sianu:12341234@localhost:5432/db_example_test?schema=public"
|
|
4
4
|
JWT_SECRET="replace_this_with_a_secure_random_string"
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
REDIS_URL
|
|
6
|
+
# Redis Configuration (Optional)
|
|
7
|
+
# If REDIS_URL is not set or connection fails, the framework will automatically
|
|
8
|
+
# switch to an in-memory Redis mock (bundled). No installation required for development.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# REDIS_URL="redis://localhost:6379"
|
|
12
|
+
|
|
13
|
+
# To force disable Redis and use in-memory mock even if Redis is available:
|
|
14
|
+
# NO_REDIS="true"
|
|
8
15
|
|
|
9
16
|
# mysql example:
|
|
10
17
|
# DATABASE_PROVIDER="mysql"
|
package/bin/index.js
CHANGED
|
@@ -146,7 +146,7 @@ const selectOption = async (query, options) => {
|
|
|
146
146
|
console.log('⚙️ Configuring environment...');
|
|
147
147
|
const envExamplePath = path.join(projectDir, '.env.example');
|
|
148
148
|
const envPath = path.join(projectDir, '.env');
|
|
149
|
-
const prismaBaseFile = path.join(projectDir, "prisma", "base.prisma");
|
|
149
|
+
const prismaBaseFile = path.join(projectDir, "prisma", "base.prisma.template");
|
|
150
150
|
|
|
151
151
|
if (fs.existsSync(envExamplePath)) {
|
|
152
152
|
let envContent = fs.readFileSync(envExamplePath, 'utf8');
|
|
@@ -168,9 +168,9 @@ const selectOption = async (query, options) => {
|
|
|
168
168
|
|
|
169
169
|
fs.writeFileSync(envPath, envContent);
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
// Update prisma/base.prisma
|
|
173
|
-
console.log("📄 Updating prisma/base.prisma...");
|
|
171
|
+
|
|
172
|
+
// Update prisma/base.prisma.template
|
|
173
|
+
console.log("📄 Updating prisma/base.prisma.template...");
|
|
174
174
|
if (fs.existsSync(prismaBaseFile)) {
|
|
175
175
|
let baseContent = fs.readFileSync(prismaBaseFile, "utf8");
|
|
176
176
|
// Replace provider in datasource block
|
package/document.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Lapeh Framework For Website Documentation
|
|
2
|
+
|
|
3
|
+
Jika Anda ingin membangun website dokumentasi sekelas **NestJS** atau **Next.js** di `https://lapeh.web.id`, berikut adalah **Blueprint Lengkap** (Rancang Bangun) yang perlu Anda siapkan.
|
|
4
|
+
|
|
5
|
+
Tujuannya adalah membuat pengguna baru merasa _terbimbing_ dari nol hingga mahir, serta memberikan referensi cepat bagi pengguna lama.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Rekomendasi Teknologi (Tech Stack)
|
|
10
|
+
|
|
11
|
+
Untuk membuat dokumentasi yang modern, cepat, dan mudah dikelola, jangan membuatnya dari nol (HTML manual). Gunakan _Documentation Framework_:
|
|
12
|
+
|
|
13
|
+
- **Pilihan Utama (React/Next.js ecosystem):** [Nextra](https://nextra.site/) atau [Docusaurus](https://docusaurus.io/).
|
|
14
|
+
- _Alasan:_ Mendukung Markdown/MDX, SEO friendly, pencarian cepat, dan tampilan yang sangat mirip dengan Next.js/Vercel docs.
|
|
15
|
+
- **Alternatif (Vue ecosystem):** [VitePress](https://vitepress.dev/).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Struktur Menu Utama (Top Navigation)
|
|
20
|
+
|
|
21
|
+
Menu di bagian atas (Header) harus sederhana namun mencakup akses ke area vital:
|
|
22
|
+
|
|
23
|
+
1. **Docs** (Dokumentasi utama)
|
|
24
|
+
2. **API Reference** (Kamus kode: daftar lengkap class, function, interface)
|
|
25
|
+
3. **Blog** (Berita rilis versi baru, tutorial kasus nyata)
|
|
26
|
+
4. **Community** (Link ke Discord, GitHub Discussions)
|
|
27
|
+
5. **GitHub Icon** (Link ke repository)
|
|
28
|
+
6. **Search Bar** (Pencarian global - _Wajib ada_)
|
|
29
|
+
7. **Version Dropdown** (Jika nanti ada v2.0, v3.0, user bisa ganti versi)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 3. Struktur Konten (Sidebar Menu)
|
|
34
|
+
|
|
35
|
+
Ini adalah "Jantung" dari dokumentasi Anda. Urutannya harus logis: dari pengenalan -> dasar -> teknik lanjut -> deploy.
|
|
36
|
+
|
|
37
|
+
### A. Introduction (Pengenalan)
|
|
38
|
+
|
|
39
|
+
- **Overview**: Apa itu Lapeh? Kenapa menggunakan ini? (Filosofi: Struktur Laravel di Node.js).
|
|
40
|
+
- **Installation**: Cara install via `npx lapeh@latest`.
|
|
41
|
+
- **First Steps**: "Hello World" pertama, menjalankan `npm run dev`, struktur folder awal.
|
|
42
|
+
- **CLI Commands**: Penjelasan perintah `lapeh`, `npm run make:module`, dll.
|
|
43
|
+
|
|
44
|
+
### B. Fundamentals (Dasar-Dasar)
|
|
45
|
+
|
|
46
|
+
- **Directory Structure**: Penjelasan detail folder `src/`, `prisma/`, `bin/`.
|
|
47
|
+
- **Routing**: Cara membuat route baru di `src/routes/`.
|
|
48
|
+
- **Controllers**: Cara membuat controller, menangani Request/Response.
|
|
49
|
+
- **Services**: Business logic layer (pemisahan logic dari controller).
|
|
50
|
+
- **Middleware**: Cara kerja middleware, error handling global, validation.
|
|
51
|
+
- **DTO & Validation**: Validasi request menggunakan **Zod**.
|
|
52
|
+
|
|
53
|
+
### C. Database (Prisma ORM)
|
|
54
|
+
|
|
55
|
+
- **Prisma Basics**: Konfigurasi `.env`, `prisma/base.prisma`.
|
|
56
|
+
- **Models**: Cara membuat model baru di `src/models/*.prisma`.
|
|
57
|
+
- **Migrations**: Workflow `npm run prisma:migrate` vs `prisma:deploy`.
|
|
58
|
+
- **Seeding**: Cara mengisi data awal (`npm run db:seed`).
|
|
59
|
+
- **Studio**: Mengelola data via GUI (`npm run db:studio`).
|
|
60
|
+
|
|
61
|
+
### D. Security (Keamanan)
|
|
62
|
+
|
|
63
|
+
- **Authentication**: Login, Register, JWT Strategy.
|
|
64
|
+
- **Authorization (RBAC)**: Role-based access control (Admin vs User).
|
|
65
|
+
- **Encryption**: Hashing password (Bcrypt).
|
|
66
|
+
- **Protection**: Helmet, CORS, Rate Limiting (sudah built-in di Lapeh).
|
|
67
|
+
|
|
68
|
+
### E. Techniques (Teknik Lanjut)
|
|
69
|
+
|
|
70
|
+
- **File Upload**: Upload gambar/file (Multer).
|
|
71
|
+
- **Realtime**: Integrasi Socket.io (karena sudah ada di `src/realtime.ts`).
|
|
72
|
+
- **Caching**: Redis integration.
|
|
73
|
+
- **Logging**: Cara logging error dan activity.
|
|
74
|
+
- **Unit Testing**: (Jika ada fitur testing).
|
|
75
|
+
|
|
76
|
+
### F. Deployment (Rilis)
|
|
77
|
+
|
|
78
|
+
- **Environment Variables**: Persiapan `.env` untuk production.
|
|
79
|
+
- **Process Manager**: Menggunakan PM2.
|
|
80
|
+
- **Docker**: Cara containerize aplikasi Lapeh.
|
|
81
|
+
- **Cloud Platforms**: Panduan deploy ke VPS (Ubuntu), Railway, atau Vercel.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4. Fitur Wajib di Website Dokumentasi
|
|
86
|
+
|
|
87
|
+
Agar website dokumentasi Anda terasa "Profesional" dan "Developer Friendly":
|
|
88
|
+
|
|
89
|
+
1. **Code Highlighting & Copy Button**:
|
|
90
|
+
Setiap blok kode harus punya warna syntax (highlighting) dan tombol "Copy" di pojok kanan atas.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Contoh tombol copy harus ada di sini
|
|
94
|
+
const app = lapeh();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
2. **Dark Mode**:
|
|
98
|
+
Developer suka mode gelap. Pastikan website mendukung toggle Light/Dark mode.
|
|
99
|
+
|
|
100
|
+
3. **Algolia Search (Pencarian Cepat)**:
|
|
101
|
+
User tidak suka klik menu satu per satu. Mereka ingin ketik "JWT" dan langsung ketemu halamannya.
|
|
102
|
+
|
|
103
|
+
4. **Prev/Next Pagination**:
|
|
104
|
+
Di bawah setiap artikel, ada tombol untuk lanjut ke bab berikutnya.
|
|
105
|
+
|
|
106
|
+
- _< Sebelumnya: Installation_ | _Selanjutnya: First Steps >_
|
|
107
|
+
|
|
108
|
+
5. **"Edit this page on GitHub"**:
|
|
109
|
+
Tombol di setiap halaman agar komunitas bisa bantu koreksi typo atau update dokumentasi (Open Source contribution).
|
|
110
|
+
|
|
111
|
+
6. **Interactive Tabs**:
|
|
112
|
+
Jika ada pilihan (misal: NPM vs Yarn vs PNPM), gunakan tab.
|
|
113
|
+
```
|
|
114
|
+
[NPM] [Yarn] [PNPM]
|
|
115
|
+
npm install lapeh
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 5. Contoh Konten Halaman Utama (Landing Page)
|
|
121
|
+
|
|
122
|
+
Halaman depan `https://lapeh.web.id` jangan langsung masuk ke dokumentasi teknis. Buat _Selling Point_:
|
|
123
|
+
|
|
124
|
+
- **Hero Section**:
|
|
125
|
+
- Judul Besar: "The Standardized Node.js Framework".
|
|
126
|
+
- Sub-judul: "Build scalable APIs with ease. Inspired by Laravel, powered by Express & Prisma."
|
|
127
|
+
- Tombol: [Get Started] [GitHub].
|
|
128
|
+
- **Features Grid**:
|
|
129
|
+
- 📦 **Modular**: Terstruktur rapi per fitur.
|
|
130
|
+
- 🛡️ **Type-Safe**: Full TypeScript & Zod.
|
|
131
|
+
- ⚡ **Prisma Power**: Database ORM modern.
|
|
132
|
+
- 🚀 **CLI Tools**: Generate code dalam detik.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 6. Langkah Kerja (Action Plan)
|
|
137
|
+
|
|
138
|
+
1. **Setup Repo**: Buat repo baru `lapeh-docs`.
|
|
139
|
+
2. **Init Project**: `npx create-nextra-app` (paling cepat) atau `npx create-docusaurus@latest`.
|
|
140
|
+
3. **Isi Konten**: Pindahkan isi `readme.md` dan `framework.md` ke dalam struktur bab di atas.
|
|
141
|
+
4. **Deploy**: Push ke GitHub, connect ke **Vercel** (gratis untuk open source).
|
|
142
|
+
5. **Domain**: Beli/set `lapeh.web.id` arahkan ke Vercel.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 7. Struktur Folder Proyek Dokumentasi (Nextra/Docusaurus)
|
|
147
|
+
|
|
148
|
+
Berikut adalah gambaran struktur folder jika Anda menggunakan **Nextra** (berbasis Next.js) untuk website dokumentasi `lapeh.web.id`:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
lapeh-docs/
|
|
152
|
+
├── pages/
|
|
153
|
+
│ ├── index.mdx # Halaman Landing Page (Home)
|
|
154
|
+
│ ├── _meta.json # Konfigurasi Menu Sidebar & Top Nav
|
|
155
|
+
│ ├── docs/ # Folder Utama Dokumentasi
|
|
156
|
+
│ │ ├── _meta.json # Urutan menu sidebar
|
|
157
|
+
│ │ ├── introduction/
|
|
158
|
+
│ │ │ ├── _meta.json
|
|
159
|
+
│ │ │ ├── overview.mdx # Apa itu Lapeh?
|
|
160
|
+
│ │ │ ├── installation.mdx
|
|
161
|
+
│ │ │ ├── first-steps.mdx
|
|
162
|
+
│ │ │ └── cli.mdx
|
|
163
|
+
│ │ ├── fundamentals/
|
|
164
|
+
│ │ │ ├── _meta.json
|
|
165
|
+
│ │ │ ├── directory-structure.mdx
|
|
166
|
+
│ │ │ ├── routing.mdx
|
|
167
|
+
│ │ │ ├── controllers.mdx
|
|
168
|
+
│ │ │ └── ...
|
|
169
|
+
│ │ ├── database/
|
|
170
|
+
│ │ │ ├── _meta.json
|
|
171
|
+
│ │ │ ├── prisma-basics.mdx
|
|
172
|
+
│ │ │ ├── models.mdx
|
|
173
|
+
│ │ │ └── ...
|
|
174
|
+
│ │ ├── security/
|
|
175
|
+
│ │ │ ├── _meta.json
|
|
176
|
+
│ │ │ └── ...
|
|
177
|
+
│ │ ├── techniques/
|
|
178
|
+
│ │ │ ├── _meta.json
|
|
179
|
+
│ │ │ └── ...
|
|
180
|
+
│ │ └── deployment/
|
|
181
|
+
│ │ ├── _meta.json
|
|
182
|
+
│ │ └── ...
|
|
183
|
+
│ ├── blog/ # Folder Blog
|
|
184
|
+
│ │ ├── _meta.json
|
|
185
|
+
│ │ ├── release-v1-0-0.mdx
|
|
186
|
+
│ │ └── tutorial-auth.mdx
|
|
187
|
+
│ └── about.mdx # Halaman About/Team
|
|
188
|
+
├── public/ # Aset statis (Logo, Gambar)
|
|
189
|
+
│ ├── logo.png
|
|
190
|
+
│ ├── favicon.ico
|
|
191
|
+
│ └── images/
|
|
192
|
+
│ └── architecture-diagram.png
|
|
193
|
+
├── theme.config.jsx # Konfigurasi Tema (Logo, Footer, Social Links)
|
|
194
|
+
├── next.config.js # Config Next.js
|
|
195
|
+
├── package.json
|
|
196
|
+
└── tsconfig.json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Penjelasan File Penting:
|
|
200
|
+
|
|
201
|
+
- **`pages/`**: Semua konten Markdown/MDX ada di sini.
|
|
202
|
+
- **`_meta.json`**: File json kecil di setiap folder untuk mengatur urutan menu sidebar dan judul yang tampil.
|
|
203
|
+
- **`theme.config.jsx`**: Di sini Anda mengatur logo `Lapeh`, link GitHub, dan konfigurasi SEO.
|
|
204
|
+
|
|
205
|
+
Dengan struktur ini, framework Anda akan terlihat sangat matang dan profesional, meningkatkan kepercayaan developer untuk menggunakannya.
|
package/framework.md
CHANGED
|
@@ -43,7 +43,7 @@ Framework ini menggunakan **Prisma ORM** dengan struktur schema yang modular (di
|
|
|
43
43
|
Saat mengembangkan aplikasi di local environment:
|
|
44
44
|
|
|
45
45
|
**a. Mengupdate Schema Database**
|
|
46
|
-
Jika Anda mengubah file schema di `src/models/*.prisma` atau `prisma/
|
|
46
|
+
Jika Anda mengubah file schema di `src/models/*.prisma` atau konfigurasi di `prisma/base.prisma.template`:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
npm run prisma:migrate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lapeh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Framework API Express yang siap pakai (Standardized)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"express-rate-limit": "8.2.1",
|
|
51
51
|
"helmet": "8.1.0",
|
|
52
52
|
"ioredis": "5.8.2",
|
|
53
|
+
"ioredis-mock": "^8.13.1",
|
|
53
54
|
"jsonwebtoken": "9.0.3",
|
|
54
55
|
"multer": "2.0.2",
|
|
55
56
|
"pg": "8.16.3",
|
package/prisma/schema.prisma
CHANGED
package/readme.md
CHANGED
|
@@ -9,21 +9,38 @@
|
|
|
9
9
|
- **Prisma ORM**: Integrasi database yang modern dan type-safe.
|
|
10
10
|
- **Schema Terpisah**: Mendukung pemisahan schema Prisma per model (mirip Eloquent).
|
|
11
11
|
- **Generator Tools**: CLI commands untuk generate Module dan Model dengan cepat.
|
|
12
|
+
- **Zero-Config Redis**: Otomatis menggunakan Redis jika tersedia, atau fallback ke in-memory mock tanpa konfigurasi.
|
|
12
13
|
- **Security Best Practices**: Dilengkapi dengan Helmet, Rate Limiting, CORS, dan JWT Authentication.
|
|
13
14
|
- **Validasi Data**: Menggunakan Zod untuk validasi request yang kuat.
|
|
14
15
|
|
|
15
16
|
## 📦 Instalasi & Penggunaan
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Anda dapat menginstall framework ini menggunakan versi terbaru atau versi spesifik agar lebih fleksibel:
|
|
19
|
+
|
|
20
|
+
### 1. Menggunakan Versi Terbaru (Recommended)
|
|
18
21
|
|
|
19
22
|
```bash
|
|
20
|
-
npx lapeh nama-project-anda
|
|
23
|
+
npx lapeh@latest nama-project-anda
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
Atau gunakan flag `--full` untuk setup lengkap (termasuk seeding data default user & roles):
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
npx lapeh nama-project-anda --full
|
|
29
|
+
npx lapeh@latest nama-project-anda --full
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Menggunakan Versi Spesifik
|
|
33
|
+
|
|
34
|
+
Jika Anda membutuhkan versi tertentu (misalnya untuk kompatibilitas):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx lapeh@1.0.8 nama-project-anda
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Atau dengan setup lengkap:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx lapeh@1.0.8 nama-project-anda --full
|
|
27
44
|
```
|
|
28
45
|
|
|
29
46
|
### Apa yang terjadi otomatis?
|
|
@@ -138,7 +155,7 @@ src/
|
|
|
138
155
|
└── index.ts # App Entry Point
|
|
139
156
|
prisma/
|
|
140
157
|
├── schema.prisma # [GENERATED] Jangan edit file ini
|
|
141
|
-
└── base.prisma
|
|
158
|
+
└── base.prisma.template # Konfigurasi Datasource & Generator
|
|
142
159
|
```
|
|
143
160
|
|
|
144
161
|
## 📝 Lisensi
|
package/scripts/check-update.js
CHANGED
|
@@ -82,6 +82,9 @@ function showUpdateMessage(latest, current) {
|
|
|
82
82
|
console.log(`${fgYellow}│ │${reset}`);
|
|
83
83
|
console.log(`${fgYellow}│ Silakan cek repository untuk melihat perubahan terbaru. │${reset}`);
|
|
84
84
|
console.log(`${fgYellow}│ │${reset}`);
|
|
85
|
+
console.log(`${fgYellow}│ Untuk upgrade jalankan: │${reset}`);
|
|
86
|
+
console.log(`${fgYellow}│ ${fgCyan}npm install lapeh@latest${reset}${fgYellow} │${reset}`);
|
|
87
|
+
console.log(`${fgYellow}│ │${reset}`);
|
|
85
88
|
console.log(`${fgYellow}└─────────────────────────────────────────────────────────────┘${reset}`);
|
|
86
89
|
console.log('\n');
|
|
87
90
|
}
|
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const prismaDir = path.join(__dirname, '..', 'prisma');
|
|
5
5
|
const modelsDir = path.join(__dirname, '..', 'src', 'models');
|
|
6
6
|
const schemaFile = path.join(prismaDir, 'schema.prisma');
|
|
7
|
-
const baseFile = path.join(prismaDir, 'base.prisma');
|
|
7
|
+
const baseFile = path.join(prismaDir, 'base.prisma.template');
|
|
8
8
|
|
|
9
9
|
// Ensure models directory exists
|
|
10
10
|
if (!fs.existsSync(modelsDir)) {
|
package/scripts/init-project.js
CHANGED
|
@@ -6,7 +6,7 @@ const readline = require("readline");
|
|
|
6
6
|
const rootDir = path.join(__dirname, "..");
|
|
7
7
|
const envExample = path.join(rootDir, ".env.example");
|
|
8
8
|
const envFile = path.join(rootDir, ".env");
|
|
9
|
-
const prismaBaseFile = path.join(rootDir, "prisma", "base.prisma");
|
|
9
|
+
const prismaBaseFile = path.join(rootDir, "prisma", "base.prisma.template");
|
|
10
10
|
|
|
11
11
|
const rl = readline.createInterface({
|
|
12
12
|
input: process.stdin,
|
|
@@ -104,8 +104,8 @@ const selectOption = async (query, options) => {
|
|
|
104
104
|
fs.writeFileSync(envFile, envContent);
|
|
105
105
|
console.log("✅ .env updated with database configuration.");
|
|
106
106
|
|
|
107
|
-
// 2. Update prisma/base.prisma
|
|
108
|
-
console.log("📄 Updating prisma/base.prisma...");
|
|
107
|
+
// 2. Update prisma/base.prisma.template
|
|
108
|
+
console.log("📄 Updating prisma/base.prisma.template...");
|
|
109
109
|
if (fs.existsSync(prismaBaseFile)) {
|
|
110
110
|
let baseContent = fs.readFileSync(prismaBaseFile, "utf8");
|
|
111
111
|
// Replace provider in datasource block
|
|
@@ -115,7 +115,7 @@ const selectOption = async (query, options) => {
|
|
|
115
115
|
);
|
|
116
116
|
fs.writeFileSync(prismaBaseFile, baseContent);
|
|
117
117
|
} else {
|
|
118
|
-
console.warn("⚠️ prisma/base.prisma not found. Skipping.");
|
|
118
|
+
console.warn("⚠️ prisma/base.prisma.template not found. Skipping.");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// 3. Install dependencies
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ dotenv.config();
|
|
|
3
3
|
import { app } from "./server";
|
|
4
4
|
import http from "http";
|
|
5
5
|
import { initRealtime } from "./realtime";
|
|
6
|
-
import {
|
|
6
|
+
import { initRedis } from "./redis";
|
|
7
7
|
|
|
8
8
|
const port = process.env.PORT ? Number(process.env.PORT) : 4000;
|
|
9
9
|
const server = http.createServer(app);
|
|
@@ -12,18 +12,8 @@ initRealtime(server);
|
|
|
12
12
|
|
|
13
13
|
server.listen(port, () => {
|
|
14
14
|
(async () => {
|
|
15
|
-
if
|
|
16
|
-
|
|
17
|
-
} else {
|
|
18
|
-
const ok = await pingRedis();
|
|
19
|
-
if (ok) {
|
|
20
|
-
console.log(`Redis connected at ${process.env.REDIS_URL}`);
|
|
21
|
-
} else {
|
|
22
|
-
console.log(
|
|
23
|
-
`Redis configured at ${process.env.REDIS_URL}, but not reachable. Using in-memory cache`
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
15
|
+
// Initialize Redis transparently (no logs if missing)
|
|
16
|
+
await initRedis();
|
|
27
17
|
console.log(`API running at http://localhost:${port}`);
|
|
28
18
|
})();
|
|
29
19
|
});
|
package/src/middleware/error.ts
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from "express"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import { Prisma } from "../../generated/prisma/client";
|
|
4
|
+
import { sendError } from "../utils/response";
|
|
5
|
+
|
|
6
|
+
export function errorHandler(
|
|
7
|
+
err: any,
|
|
8
|
+
_req: Request,
|
|
9
|
+
res: Response,
|
|
10
|
+
_next: NextFunction
|
|
11
|
+
) {
|
|
12
|
+
// 1. Zod Validation Error
|
|
13
|
+
if (err instanceof ZodError) {
|
|
14
|
+
const formattedErrors = err.errors.map((e) => ({
|
|
15
|
+
field: e.path.join("."),
|
|
16
|
+
message: e.message,
|
|
17
|
+
}));
|
|
18
|
+
return sendError(res, 400, "Validation Error", formattedErrors);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Prisma Errors
|
|
22
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
23
|
+
// P2002: Unique constraint failed
|
|
24
|
+
if (err.code === "P2002") {
|
|
25
|
+
const target = (err.meta?.target as string[]) || [];
|
|
26
|
+
const fields = target.length > 0 ? target.join(", ") : "field";
|
|
27
|
+
return sendError(res, 409, `Unique constraint failed on: ${fields}`);
|
|
28
|
+
}
|
|
29
|
+
// P2025: Record not found
|
|
30
|
+
if (err.code === "P2025") {
|
|
31
|
+
return sendError(res, 404, "Record not found");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. JWT Errors
|
|
36
|
+
if (err.name === "JsonWebTokenError") {
|
|
37
|
+
return sendError(res, 401, "Invalid token");
|
|
38
|
+
}
|
|
39
|
+
if (err.name === "TokenExpiredError") {
|
|
40
|
+
return sendError(res, 401, "Token expired");
|
|
41
|
+
}
|
|
8
42
|
|
|
43
|
+
// 4. Syntax Error (JSON body parsing)
|
|
44
|
+
if (err instanceof SyntaxError && "body" in err) {
|
|
45
|
+
return sendError(res, 400, "Invalid JSON format");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5. Default / Custom Error
|
|
49
|
+
const code = err.statusCode || 500;
|
|
50
|
+
const msg = err.message || "Internal Server Error";
|
|
51
|
+
|
|
52
|
+
// Log error in development for debugging
|
|
53
|
+
if (code === 500 && process.env.NODE_ENV !== "production") {
|
|
54
|
+
console.error("❌ [Global Error Handler]:", err);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sendError(res, code, msg);
|
|
58
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import { redis
|
|
3
|
+
import { redis } from "../redis";
|
|
4
4
|
|
|
5
5
|
type DayMemoryStats = {
|
|
6
6
|
requests: number;
|
|
@@ -75,7 +75,7 @@ export async function visitorCounter(
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
if (
|
|
78
|
+
if (redis && redis.status === "ready") {
|
|
79
79
|
const base = dateKey;
|
|
80
80
|
const kRequests = `requests-${base}`;
|
|
81
81
|
const kNewVisitors = `new-visitors-${base}`;
|
|
@@ -127,8 +127,7 @@ export async function visitorCounter(
|
|
|
127
127
|
if (addedSession === 1) {
|
|
128
128
|
await redis.incr(kSessions);
|
|
129
129
|
}
|
|
130
|
-
} catch {
|
|
131
|
-
}
|
|
130
|
+
} catch {}
|
|
132
131
|
} else {
|
|
133
132
|
let stats = memoryStats.get(dateKey);
|
|
134
133
|
if (!stats) {
|
|
@@ -177,4 +176,3 @@ export async function visitorCounter(
|
|
|
177
176
|
|
|
178
177
|
next();
|
|
179
178
|
}
|
|
180
|
-
|
package/src/redis.ts
CHANGED
|
@@ -1,69 +1,121 @@
|
|
|
1
|
-
import Redis from "ioredis";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
1
|
+
import Redis from "ioredis";
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import RedisMock from "ioredis-mock";
|
|
4
|
+
|
|
5
|
+
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
6
|
+
|
|
7
|
+
// Create a wrapper to handle connection attempts
|
|
8
|
+
let redis: Redis;
|
|
9
|
+
let isRedisConnected = false;
|
|
10
|
+
|
|
11
|
+
// If explicitly disabled via env
|
|
12
|
+
if (process.env.NO_REDIS === "true") {
|
|
13
|
+
console.log("Redis disabled via NO_REDIS, using in-memory mock.");
|
|
14
|
+
redis = new RedisMock();
|
|
15
|
+
isRedisConnected = true;
|
|
16
|
+
} else {
|
|
17
|
+
// Try to connect to real Redis
|
|
18
|
+
redis = new Redis(redisUrl, {
|
|
19
|
+
lazyConnect: true,
|
|
20
|
+
maxRetriesPerRequest: 1,
|
|
21
|
+
retryStrategy: (times) => {
|
|
22
|
+
// Retry 3 times then give up
|
|
23
|
+
if (times > 3) return null;
|
|
24
|
+
return 200;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
redis.on("ready", () => {
|
|
30
|
+
isRedisConnected = true;
|
|
31
|
+
// console.log("Redis connected!");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
redis.on("error", (err) => {
|
|
35
|
+
// If connection fails and we haven't switched to mock yet
|
|
36
|
+
if (!isRedisConnected && !(redis instanceof RedisMock)) {
|
|
37
|
+
// console.log("Redis connection failed, switching to in-memory mock...");
|
|
38
|
+
// Replace the global redis instance with mock
|
|
39
|
+
// Note: This is a runtime switch. Existing listeners might be lost if we don't handle carefully.
|
|
40
|
+
// However, for a simple fallback, we can just use the mock for future calls.
|
|
41
|
+
|
|
42
|
+
// Better approach: Since we exported 'redis' as a const (reference), we can't reassign it easily
|
|
43
|
+
// if other modules already imported it.
|
|
44
|
+
// BUT, ioredis instance itself is an EventEmitter.
|
|
45
|
+
|
|
46
|
+
// Strategy: We keep 'redis' as the main interface.
|
|
47
|
+
// If real redis fails, we just don't set isRedisConnected to true for the *real* one.
|
|
48
|
+
// But wait, the user wants 'bundle redis'.
|
|
49
|
+
// The best way is to detect failure during init and SWAP the implementation.
|
|
50
|
+
}
|
|
51
|
+
isRedisConnected = false;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// We need a way to seamlessly switch or just default to Mock if connect fails.
|
|
55
|
+
// Since 'redis' is exported immediately, we can't easily swap the object reference for importers.
|
|
56
|
+
// PROXY APPROACH:
|
|
57
|
+
// We export a Proxy that forwards to real redis OR mock redis.
|
|
58
|
+
|
|
59
|
+
const mockRedis = new RedisMock();
|
|
60
|
+
let activeRedis = redis; // Start with real redis attempt
|
|
61
|
+
|
|
62
|
+
// Custom init function to determine which one to use
|
|
63
|
+
export async function initRedis() {
|
|
64
|
+
if (process.env.NO_REDIS === "true") {
|
|
65
|
+
activeRedis = mockRedis;
|
|
66
|
+
if (process.env.NODE_ENV === "production") {
|
|
67
|
+
console.warn(
|
|
68
|
+
"⚠️ WARNING: Running in PRODUCTION with in-memory Redis mock. Data will be lost on restart and not shared between instances."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await redis.connect();
|
|
76
|
+
activeRedis = redis; // Keep using real redis
|
|
77
|
+
isRedisConnected = true;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// Connection failed, switch to mock
|
|
80
|
+
// console.log("Redis failed, using in-memory mock");
|
|
81
|
+
activeRedis = mockRedis;
|
|
82
|
+
isRedisConnected = true; // Mock is always "connected"
|
|
83
|
+
if (process.env.NODE_ENV === "production") {
|
|
84
|
+
console.warn(
|
|
85
|
+
"⚠️ WARNING: Redis connection failed in PRODUCTION. Switched to in-memory mock. Data will be lost on restart."
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Proxy handler to forward all calls to activeRedis
|
|
92
|
+
const redisProxy = new Proxy({} as Redis, {
|
|
93
|
+
get: (target, prop) => {
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
return activeRedis[prop];
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export async function getCache(key: string) {
|
|
100
|
+
try {
|
|
101
|
+
const v = await activeRedis.get(key);
|
|
102
|
+
return v ? JSON.parse(v) : null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function setCache(key: string, value: any, ttlSeconds = 60) {
|
|
109
|
+
try {
|
|
110
|
+
await activeRedis.set(key, JSON.stringify(value), "EX", ttlSeconds);
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function delCache(key: string) {
|
|
115
|
+
try {
|
|
116
|
+
await activeRedis.del(key);
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Export the proxy as 'redis' so consumers use it transparently
|
|
121
|
+
export { redisProxy as redis };
|
|
File without changes
|