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 CHANGED
@@ -1,10 +1,17 @@
1
1
  PORT=4000
2
2
  DATABASE_PROVIDER="postgresql"
3
- DATABASE_URL="postgresql://roby:12341234@localhost:5432/db_example_test?schema=public"
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
- # redis example:
7
- REDIS_URL="redis://localhost:6379"
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/schema.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.8",
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",
@@ -1,5 +1,5 @@
1
1
  generator client {
2
- provider = "postgresql"
2
+ provider = "prisma-client-js"
3
3
  output = "../generated/prisma"
4
4
  }
5
5
 
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
- Buat project baru cukup dengan satu perintah:
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 # Konfigurasi Datasource & Generator
158
+ └── base.prisma.template # Konfigurasi Datasource & Generator
142
159
  ```
143
160
 
144
161
  ## 📝 Lisensi
@@ -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)) {
@@ -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 { useRedis, pingRedis } from "./redis";
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 (!useRedis) {
16
- console.log("Redis not configured, using in-memory cache");
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
  });
@@ -1,8 +1,58 @@
1
- import { Request, Response, NextFunction } from "express"
2
- import { sendError } from "../utils/response"
3
- export function errorHandler(err: any, _req: Request, res: Response, _next: NextFunction) {
4
- const code = err.statusCode || 500
5
- const msg = err.message || "Internal Server Error"
6
- sendError(res, code, msg)
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, useRedis } from "../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 (useRedis && redis) {
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
- export const useRedis = !!process.env.REDIS_URL;
4
-
5
- let redis: Redis | null = null;
6
- if (useRedis) {
7
- redis = new Redis(process.env.REDIS_URL as string, {
8
- lazyConnect: true,
9
- maxRetriesPerRequest: 0,
10
- enableOfflineQueue: false,
11
- });
12
- }
13
-
14
- const memory = new Map<string, { value: any; expireAt: number }>();
15
-
16
- export async function getCache(key: string) {
17
- if (useRedis && redis) {
18
- try {
19
- const v = await redis.get(key);
20
- return v ? JSON.parse(v) : null;
21
- } catch {
22
- // fall through
23
- }
24
- } else {
25
- const entry = memory.get(key);
26
- if (entry && entry.expireAt > Date.now()) return entry.value;
27
- if (entry) memory.delete(key);
28
- }
29
- return null;
30
- }
31
-
32
- export async function setCache(key: string, value: any, ttlSeconds = 60) {
33
- if (useRedis && redis) {
34
- try {
35
- await redis.set(key, JSON.stringify(value), "EX", ttlSeconds);
36
- return;
37
- } catch {
38
- // fall through
39
- }
40
- } else {
41
- memory.set(key, { value, expireAt: Date.now() + ttlSeconds * 1000 });
42
- }
43
- }
44
-
45
- export async function delCache(key: string) {
46
- if (useRedis && redis) {
47
- try {
48
- await redis.del(key);
49
- return;
50
- } catch {
51
- // fall through
52
- }
53
- } else {
54
- memory.delete(key);
55
- }
56
- }
57
-
58
- export async function pingRedis(): Promise<boolean> {
59
- if (!useRedis || !redis) return false;
60
- try {
61
- await redis.connect();
62
- const res = await redis.ping();
63
- return res === "PONG";
64
- } catch {
65
- return false;
66
- }
67
- }
68
-
69
- export { redis };
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