lapeh 2.0.0 → 2.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 CHANGED
@@ -3,12 +3,11 @@ DATABASE_PROVIDER="postgresql"
3
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 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"
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
+ # REDIS_URL="redis://lapeh:12341234@localhost:6379"
10
+ # NO_REDIS="true"
12
11
 
13
12
  # To force disable Redis and use in-memory mock even if Redis is available:
14
13
  # NO_REDIS="true"
package/bin/index.js CHANGED
@@ -6,228 +6,330 @@ const { execSync } = require('child_process');
6
6
  const readline = require('readline');
7
7
 
8
8
  const args = process.argv.slice(2);
9
- const projectName = args.find(arg => !arg.startsWith('-'));
10
- const isFull = args.includes('--full');
11
9
 
12
- if (!projectName) {
13
- console.error('❌ Please specify the project name:');
14
- console.error(' npx lapeh-cli <project-name> [--full]');
15
- process.exit(1);
10
+ // --- UPGRADE MODE ---
11
+ if (args[0] === 'upgrade') {
12
+ (async () => {
13
+ await upgradeProject();
14
+ })();
15
+ } else {
16
+ // --- CREATE MODE ---
17
+ createProject();
16
18
  }
17
19
 
18
- const currentDir = process.cwd();
19
- const projectDir = path.join(currentDir, projectName);
20
- const templateDir = path.join(__dirname, '..');
21
-
22
- if (fs.existsSync(projectDir)) {
23
- console.error(`❌ Directory ${projectName} already exists.`);
24
- process.exit(1);
25
- }
26
-
27
- // Setup readline interface
28
- const rl = readline.createInterface({
29
- input: process.stdin,
30
- output: process.stdout,
31
- });
32
-
33
- const ask = (query, defaultVal) => {
34
- return new Promise((resolve) => {
35
- rl.question(`${query} ${defaultVal ? `[${defaultVal}]` : ""}: `, (answer) => {
36
- resolve(answer.trim() || defaultVal);
37
- });
38
- });
39
- };
40
-
41
- const selectOption = async (query, options) => {
42
- console.log(query);
43
- options.forEach((opt, idx) => {
44
- console.log(` [${opt.key}] ${opt.label}`);
45
- });
20
+ async function upgradeProject() {
21
+ const currentDir = process.cwd();
22
+ const templateDir = path.join(__dirname, '..');
46
23
 
47
- while (true) {
48
- const answer = await ask(">", options[0].key);
49
- const selected = options.find(o => o.key.toLowerCase() === answer.toLowerCase());
50
- if (selected) return selected;
51
-
52
- const byLabel = options.find(o => o.label.toLowerCase().includes(answer.toLowerCase()));
53
- if (byLabel) return byLabel;
24
+ console.log(`🚀 Upgrading Lapeh project in ${currentDir}...`);
54
25
 
55
- console.log("Pilihan tidak valid. Silakan coba lagi.");
26
+ // Check if package.json exists
27
+ const packageJsonPath = path.join(currentDir, 'package.json');
28
+ if (!fs.existsSync(packageJsonPath)) {
29
+ console.error('❌ No package.json found. Are you in the root of a Lapeh project?');
30
+ process.exit(1);
56
31
  }
57
- };
58
-
59
- (async () => {
60
- console.log(`🚀 Creating a new API Lapeh project in ${projectDir}...`);
61
- fs.mkdirSync(projectDir);
62
-
63
- // --- DATABASE SELECTION ---
64
- console.log("\n--- Database Configuration ---");
65
- const dbType = await selectOption("Database apa yang akan digunakan?", [
66
- { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" },
67
- { key: "mysql", label: "MySQL", provider: "mysql", defaultPort: "3306" },
68
- { key: "mariadb", label: "MariaDB", provider: "mysql", defaultPort: "3306" },
69
- { key: "sqlite", label: "SQLite", provider: "sqlite", defaultPort: "" },
70
- ]);
71
-
72
- let dbUrl = "";
73
- let dbProvider = dbType.provider;
74
-
75
- if (dbType.key === "sqlite") {
76
- dbUrl = "file:./dev.db";
77
- } else {
78
- const host = await ask("Database Host", "localhost");
79
- const port = await ask("Database Port", dbType.defaultPort);
80
- const user = await ask("Database User", "root");
81
- const password = await ask("Database Password", "");
82
- const dbName = await ask("Database Name", projectName.replace(/-/g, '_')); // Default db name based on project name
83
-
84
- if (dbType.key === "pgsql") {
85
- dbUrl = `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
32
+
33
+ // Files/Folders to overwrite/copy
34
+ const filesToSync = [
35
+ 'scripts',
36
+ 'docker-compose.yml',
37
+ '.env.example',
38
+ '.vscode',
39
+ 'tsconfig.json',
40
+ ];
41
+
42
+ // Helper to copy recursive
43
+ function copyRecursive(src, dest) {
44
+ if (!fs.existsSync(src)) return;
45
+ const stats = fs.statSync(src);
46
+ if (stats.isDirectory()) {
47
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest);
48
+ fs.readdirSync(src).forEach(childItemName => {
49
+ copyRecursive(path.join(src, childItemName), path.join(dest, childItemName));
50
+ });
86
51
  } else {
87
- dbUrl = `mysql://${user}:${password}@${host}:${port}/${dbName}`;
52
+ fs.copyFileSync(src, dest);
88
53
  }
89
54
  }
90
55
 
91
- rl.close();
92
-
93
- // List of files/folders to exclude
94
- const ignoreList = [
95
- 'node_modules',
96
- 'dist',
97
- '.git',
98
- '.env',
99
- 'bin', // Don't copy the CLI script itself
100
- 'package-lock.json',
101
- '.DS_Store',
102
- projectName // Don't copy the destination folder itself if creating inside the template
103
- ];
56
+ for (const item of filesToSync) {
57
+ const srcPath = path.join(templateDir, item);
58
+ const destPath = path.join(currentDir, item);
59
+
60
+ if (fs.existsSync(srcPath)) {
61
+ console.log(`🔄 Updating ${item}...`);
62
+ copyRecursive(srcPath, destPath);
63
+ }
64
+ }
104
65
 
105
- function copyDir(src, dest) {
106
- const entries = fs.readdirSync(src, { withFileTypes: true });
66
+ // Merge package.json
67
+ console.log('📝 Updating package.json...');
68
+ const currentPackageJson = require(packageJsonPath);
69
+ const templatePackageJson = require(path.join(templateDir, 'package.json'));
70
+
71
+ // Update scripts
72
+ currentPackageJson.scripts = {
73
+ ...currentPackageJson.scripts,
74
+ ...templatePackageJson.scripts
75
+ };
76
+
77
+ // Update dependencies
78
+ currentPackageJson.dependencies = {
79
+ ...currentPackageJson.dependencies,
80
+ ...templatePackageJson.dependencies
81
+ };
82
+
83
+ // Update devDependencies
84
+ currentPackageJson.devDependencies = {
85
+ ...currentPackageJson.devDependencies,
86
+ ...templatePackageJson.devDependencies
87
+ };
107
88
 
108
- for (const entry of entries) {
109
- const srcPath = path.join(src, entry.name);
110
- const destPath = path.join(dest, entry.name);
89
+ // Update Lapeh version tag
90
+ currentPackageJson.dependencies["lapeh"] = templatePackageJson.version;
111
91
 
112
- if (ignoreList.includes(entry.name)) {
113
- continue;
114
- }
92
+ fs.writeFileSync(packageJsonPath, JSON.stringify(currentPackageJson, null, 2));
115
93
 
116
- if (entry.isDirectory()) {
117
- fs.mkdirSync(destPath);
118
- copyDir(srcPath, destPath);
119
- } else {
120
- fs.copyFileSync(srcPath, destPath);
121
- }
122
- }
94
+ // Run npm install
95
+ console.log('📦 Installing updated dependencies...');
96
+ try {
97
+ execSync('npm install', { cwd: currentDir, stdio: 'inherit' });
98
+ } catch (error) {
99
+ console.error('❌ Error installing dependencies.');
100
+ process.exit(1);
123
101
  }
124
102
 
125
- console.log('\n📂 Copying template files...');
126
- copyDir(templateDir, projectDir);
103
+ console.log('\n Upgrade completed successfully!');
104
+ console.log(' Please check your .env file against .env.example for any new required variables.');
105
+ }
127
106
 
128
- // Update package.json
129
- console.log('📝 Updating package.json...');
130
- const packageJsonPath = path.join(projectDir, 'package.json');
131
- const packageJson = require(packageJsonPath);
107
+ function createProject() {
108
+ const projectName = args.find(arg => !arg.startsWith('-'));
109
+ const isFull = args.includes('--full');
132
110
 
133
- packageJson.name = projectName;
134
- // Add lapeh framework version to dependencies to track it like react-router
135
- packageJson.dependencies = packageJson.dependencies || {};
136
- packageJson.dependencies["lapeh"] = packageJson.version;
137
-
138
- packageJson.version = '1.0.0';
139
- packageJson.description = 'Generated by lapeh';
140
- delete packageJson.bin; // Remove the bin entry from the generated project
141
- delete packageJson.repository; // Remove repository info if specific to the template
111
+ if (!projectName) {
112
+ console.error('❌ Please specify the project name:');
113
+ console.error(' npx lapeh-cli <project-name> [--full]');
114
+ console.error(' OR');
115
+ console.error(' npx lapeh-cli upgrade (inside existing project)');
116
+ process.exit(1);
117
+ }
142
118
 
143
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
119
+ const currentDir = process.cwd();
120
+ const projectDir = path.join(currentDir, projectName);
121
+ const templateDir = path.join(__dirname, '..');
144
122
 
145
- // Create .env from .env.example with correct DB config
146
- console.log('⚙️ Configuring environment...');
147
- const envExamplePath = path.join(projectDir, '.env.example');
148
- const envPath = path.join(projectDir, '.env');
149
- const prismaBaseFile = path.join(projectDir, "prisma", "base.prisma.template");
123
+ if (fs.existsSync(projectDir)) {
124
+ console.error(`❌ Directory ${projectName} already exists.`);
125
+ process.exit(1);
126
+ }
127
+
128
+ // Setup readline interface
129
+ const rl = readline.createInterface({
130
+ input: process.stdin,
131
+ output: process.stdout,
132
+ });
133
+
134
+ const ask = (query, defaultVal) => {
135
+ return new Promise((resolve) => {
136
+ rl.question(`${query} ${defaultVal ? `[${defaultVal}]` : ""}: `, (answer) => {
137
+ resolve(answer.trim() || defaultVal);
138
+ });
139
+ });
140
+ };
150
141
 
151
- if (fs.existsSync(envExamplePath)) {
152
- let envContent = fs.readFileSync(envExamplePath, 'utf8');
142
+ const selectOption = async (query, options) => {
143
+ console.log(query);
144
+ options.forEach((opt, idx) => {
145
+ console.log(` [${opt.key}] ${opt.label}`);
146
+ });
153
147
 
154
- // Replace DATABASE_URL and DATABASE_PROVIDER
155
- if (envContent.includes("DATABASE_URL=")) {
156
- envContent = envContent.replace(/DATABASE_URL=".+"/g, `DATABASE_URL="${dbUrl}"`);
157
- envContent = envContent.replace(/DATABASE_URL=.+/g, `DATABASE_URL="${dbUrl}"`);
148
+ while (true) {
149
+ const answer = await ask(">", options[0].key);
150
+ const selected = options.find(o => o.key.toLowerCase() === answer.toLowerCase());
151
+ if (selected) return selected;
152
+
153
+ const byLabel = options.find(o => o.label.toLowerCase().includes(answer.toLowerCase()));
154
+ if (byLabel) return byLabel;
155
+
156
+ console.log("Pilihan tidak valid. Silakan coba lagi.");
157
+ }
158
+ };
159
+
160
+ (async () => {
161
+ console.log(`🚀 Creating a new API Lapeh project in ${projectDir}...`);
162
+ fs.mkdirSync(projectDir);
163
+
164
+ // --- DATABASE SELECTION ---
165
+ console.log("\n--- Database Configuration ---");
166
+ const dbType = await selectOption("Database apa yang akan digunakan?", [
167
+ { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" },
168
+ { key: "mysql", label: "MySQL", provider: "mysql", defaultPort: "3306" },
169
+ { key: "mariadb", label: "MariaDB", provider: "mysql", defaultPort: "3306" },
170
+ { key: "sqlite", label: "SQLite", provider: "sqlite", defaultPort: "" },
171
+ ]);
172
+
173
+ let dbUrl = "";
174
+ let dbProvider = dbType.provider;
175
+
176
+ if (dbType.key === "sqlite") {
177
+ dbUrl = "file:./dev.db";
158
178
  } else {
159
- envContent += `\nDATABASE_URL="${dbUrl}"`;
179
+ const host = await ask("Database Host", "localhost");
180
+ const port = await ask("Database Port", dbType.defaultPort);
181
+ const user = await ask("Database User", "root");
182
+ const password = await ask("Database Password", "");
183
+ const dbName = await ask("Database Name", projectName.replace(/-/g, '_')); // Default db name based on project name
184
+
185
+ if (dbType.key === "pgsql") {
186
+ dbUrl = `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
187
+ } else {
188
+ dbUrl = `mysql://${user}:${password}@${host}:${port}/${dbName}`;
189
+ }
160
190
  }
161
191
 
162
- if (envContent.includes("DATABASE_PROVIDER=")) {
163
- envContent = envContent.replace(/DATABASE_PROVIDER=".+"/g, `DATABASE_PROVIDER="${dbProvider}"`);
164
- envContent = envContent.replace(/DATABASE_PROVIDER=.+/g, `DATABASE_PROVIDER="${dbProvider}"`);
165
- } else {
166
- envContent += `\nDATABASE_PROVIDER="${dbProvider}"`;
192
+ rl.close();
193
+
194
+ // List of files/folders to exclude
195
+ const ignoreList = [
196
+ 'node_modules',
197
+ 'dist',
198
+ '.git',
199
+ '.env',
200
+ 'bin', // Don't copy the CLI script itself
201
+ 'package-lock.json',
202
+ '.DS_Store',
203
+ projectName // Don't copy the destination folder itself if creating inside the template
204
+ ];
205
+
206
+ function copyDir(src, dest) {
207
+ const entries = fs.readdirSync(src, { withFileTypes: true });
208
+
209
+ for (const entry of entries) {
210
+ const srcPath = path.join(src, entry.name);
211
+ const destPath = path.join(dest, entry.name);
212
+
213
+ if (ignoreList.includes(entry.name)) {
214
+ continue;
215
+ }
216
+
217
+ if (entry.isDirectory()) {
218
+ fs.mkdirSync(destPath);
219
+ copyDir(srcPath, destPath);
220
+ } else {
221
+ fs.copyFileSync(srcPath, destPath);
222
+ }
223
+ }
167
224
  }
168
225
 
169
- fs.writeFileSync(envPath, envContent);
170
- }
171
-
172
- // Update prisma/base.prisma.template
173
- console.log("📄 Updating prisma/base.prisma.template...");
174
- if (fs.existsSync(prismaBaseFile)) {
175
- let baseContent = fs.readFileSync(prismaBaseFile, "utf8");
176
- // Replace provider in datasource block
177
- baseContent = baseContent.replace(
178
- /(datasource\s+db\s+\{[\s\S]*?provider\s*=\s*")([^"]+)(")/,
179
- `$1${dbProvider}$3`
180
- );
181
- fs.writeFileSync(prismaBaseFile, baseContent);
182
- }
226
+ console.log('\n📂 Copying template files...');
227
+ copyDir(templateDir, projectDir);
183
228
 
184
- // Install dependencies
185
- console.log('📦 Installing dependencies (this might take a while)...');
186
- try {
187
- execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
188
- } catch (error) {
189
- console.error('❌ Error installing dependencies.');
190
- process.exit(1);
191
- }
229
+ // Update package.json
230
+ console.log('📝 Updating package.json...');
231
+ const packageJsonPath = path.join(projectDir, 'package.json');
232
+ const packageJson = require(packageJsonPath);
192
233
 
193
- // Generate JWT Secret
194
- console.log('🔑 Generating JWT Secret...');
195
- try {
196
- execSync('npm run generate:jwt', { cwd: projectDir, stdio: 'inherit' });
197
- } catch (error) {
198
- console.warn('⚠️ Could not generate JWT secret automatically.');
199
- }
200
-
201
- // Generate Prisma Client & Migrate
202
- console.log('🗄️ Setting up database...');
203
- try {
204
- console.log(' Compiling schema...');
205
- execSync('node scripts/compile-schema.js', { cwd: projectDir, stdio: 'inherit' });
234
+ packageJson.name = projectName;
235
+ // Add lapeh framework version to dependencies to track it like react-router
236
+ packageJson.dependencies = packageJson.dependencies || {};
237
+ packageJson.dependencies["lapeh"] = packageJson.version;
206
238
 
207
- console.log(' Generating Prisma Client...');
208
- execSync('npx prisma generate', { cwd: projectDir, stdio: 'inherit' });
239
+ packageJson.version = '1.0.0';
240
+ packageJson.description = 'Generated by lapeh';
241
+ delete packageJson.bin; // Remove the bin entry from the generated project
242
+ delete packageJson.repository; // Remove repository info if specific to the template
243
+
244
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
245
+
246
+ // Create .env from .env.example with correct DB config
247
+ console.log('⚙️ Configuring environment...');
248
+ const envExamplePath = path.join(projectDir, '.env.example');
249
+ const envPath = path.join(projectDir, '.env');
250
+ const prismaBaseFile = path.join(projectDir, "prisma", "base.prisma.template");
251
+
252
+ if (fs.existsSync(envExamplePath)) {
253
+ let envContent = fs.readFileSync(envExamplePath, 'utf8');
254
+
255
+ // Replace DATABASE_URL and DATABASE_PROVIDER
256
+ if (envContent.includes("DATABASE_URL=")) {
257
+ envContent = envContent.replace(/DATABASE_URL=".+"/g, `DATABASE_URL="${dbUrl}"`);
258
+ envContent = envContent.replace(/DATABASE_URL=.+/g, `DATABASE_URL="${dbUrl}"`);
259
+ } else {
260
+ envContent += `\nDATABASE_URL="${dbUrl}"`;
261
+ }
262
+
263
+ if (envContent.includes("DATABASE_PROVIDER=")) {
264
+ envContent = envContent.replace(/DATABASE_PROVIDER=".+"/g, `DATABASE_PROVIDER="${dbProvider}"`);
265
+ envContent = envContent.replace(/DATABASE_PROVIDER=.+/g, `DATABASE_PROVIDER="${dbProvider}"`);
266
+ } else {
267
+ envContent += `\nDATABASE_PROVIDER="${dbProvider}"`;
268
+ }
209
269
 
210
- // Try to migrate (this will create the DB if it doesn't exist)
211
- console.log(' Running migration (creates DB if missing)...');
212
- execSync('npx prisma migrate dev --name init_setup', { cwd: projectDir, stdio: 'inherit' });
270
+ fs.writeFileSync(envPath, envContent);
271
+ }
213
272
 
214
- // Seed
215
- if (isFull) {
216
- console.log(' Seeding database...');
217
- execSync('npm run db:seed', { cwd: projectDir, stdio: 'inherit' });
218
- } else {
219
- console.log(' ℹ️ Skipping database seeding (use --full to seed default data)...');
273
+ // Update prisma/base.prisma.template
274
+ console.log("📄 Updating prisma/base.prisma.template...");
275
+ if (fs.existsSync(prismaBaseFile)) {
276
+ let baseContent = fs.readFileSync(prismaBaseFile, "utf8");
277
+ // Replace provider in datasource block
278
+ baseContent = baseContent.replace(
279
+ /(datasource\s+db\s+\{[\s\S]*?provider\s*=\s*")([^"]+)(")/,
280
+ `$1${dbProvider}$3`
281
+ );
282
+ fs.writeFileSync(prismaBaseFile, baseContent);
220
283
  }
221
284
 
222
- } catch (error) {
223
- console.warn('⚠️ Database setup encountered an issue.');
224
- console.warn(' You may need to check your .env credentials and run:');
225
- console.warn(' cd ' + projectName);
226
- console.warn(' npm run prisma:migrate');
227
- }
285
+ // Install dependencies
286
+ console.log('📦 Installing dependencies (this might take a while)...');
287
+ try {
288
+ execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
289
+ } catch (error) {
290
+ console.error('❌ Error installing dependencies.');
291
+ process.exit(1);
292
+ }
293
+
294
+ // Generate JWT Secret
295
+ console.log('🔑 Generating JWT Secret...');
296
+ try {
297
+ execSync('npm run generate:jwt', { cwd: projectDir, stdio: 'inherit' });
298
+ } catch (error) {
299
+ console.warn('⚠️ Could not generate JWT secret automatically.');
300
+ }
228
301
 
229
- console.log(`\n✅ Project ${projectName} created successfully!`);
230
- console.log(`\nNext steps:`);
231
- console.log(` cd ${projectName}`);
232
- console.log(` npm run dev`);
233
- })();
302
+ // Generate Prisma Client & Migrate
303
+ console.log('🗄️ Setting up database...');
304
+ try {
305
+ console.log(' Compiling schema...');
306
+ execSync('node scripts/compile-schema.js', { cwd: projectDir, stdio: 'inherit' });
307
+
308
+ console.log(' Generating Prisma Client...');
309
+ execSync('npx prisma generate', { cwd: projectDir, stdio: 'inherit' });
310
+
311
+ // Try to migrate (this will create the DB if it doesn't exist)
312
+ console.log(' Running migration (creates DB if missing)...');
313
+ execSync('npx prisma migrate dev --name init_setup', { cwd: projectDir, stdio: 'inherit' });
314
+
315
+ // Seed
316
+ if (isFull) {
317
+ console.log(' Seeding database...');
318
+ execSync('npm run db:seed', { cwd: projectDir, stdio: 'inherit' });
319
+ } else {
320
+ console.log(' ℹ️ Skipping database seeding (use --full to seed default data)...');
321
+ }
322
+
323
+ } catch (error) {
324
+ console.warn('⚠️ Database setup encountered an issue.');
325
+ console.warn(' You may need to check your .env credentials and run:');
326
+ console.warn(' cd ' + projectName);
327
+ console.warn(' npm run prisma:migrate');
328
+ }
329
+
330
+ console.log(`\n✅ Project ${projectName} created successfully!`);
331
+ console.log(`\nNext steps:`);
332
+ console.log(` cd ${projectName}`);
333
+ console.log(` npm run dev`);
334
+ })();
335
+ }
@@ -2,7 +2,14 @@ version: "3.9"
2
2
  services:
3
3
  redis:
4
4
  image: redis:7-alpine
5
- command: ["redis-server", "--appendonly", "yes"]
5
+ command:
6
+ - "redis-server"
7
+ - "--appendonly"
8
+ - "yes"
9
+ - "--user"
10
+ - "default on >12341234 ~* +@all"
11
+ - "--user"
12
+ - "lapeh on >12341234 ~* +@all"
6
13
  ports:
7
14
  - "6379:6379"
8
15
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lapeh",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Framework API Express yang siap pakai (Standardized)",
5
5
  "main": "index.js",
6
6
  "bin": {
package/readme.md CHANGED
@@ -75,6 +75,63 @@ Jika Anda melakukan setup dengan flag `--full`, database akan terisi dengan akun
75
75
 
76
76
  ---
77
77
 
78
+ ## 🔄 Upgrade Project
79
+
80
+ Jika Anda memiliki project lama yang dibuat dengan versi Lapeh sebelumnya dan ingin memperbarui struktur, scripts, dan konfigurasi ke standar terbaru (termasuk keamanan Redis baru), Anda tidak perlu membuat project ulang.
81
+
82
+ Cukup jalankan perintah ini di dalam folder project Anda:
83
+
84
+ ```bash
85
+ npx lapeh@latest upgrade
86
+ ```
87
+
88
+ Perintah ini akan secara otomatis:
89
+
90
+ 1. Mengupdate `scripts/` (termasuk generator controller baru).
91
+ 2. Mengupdate `docker-compose.yml` (keamanan Redis).
92
+ 3. Mengupdate dependencies di `package.json`.
93
+ 4. Menambahkan konfigurasi `.vscode` dan `tsconfig` terbaru.
94
+
95
+ > **Catatan:** File `.env` Anda **tidak akan ditimpa**, namun kami akan mengupdate `.env.example` sebagai referensi konfigurasi terbaru.
96
+
97
+ ## 🧠 Zero-Config Redis
98
+
99
+ Lapeh otomatis mendeteksi ketersediaan Redis.
100
+
101
+ 1. **Auto-Discovery**: Mencoba terhubung ke Redis URL di `.env` (`REDIS_URL`).
102
+ 2. **Smart Fallback**: Jika Redis tidak tersedia atau koneksi gagal, otomatis beralih ke **In-Memory Mock**.
103
+ - Tidak perlu install Redis di local development.
104
+ - Fitur rate-limiting dan caching tetap berjalan (namun data hilang saat restart).
105
+ 3. **Production Safety**: Memberikan peringatan log jika berjalan di Production menggunakan Mock.
106
+
107
+ **Force Mock Mode:**
108
+ Anda bisa memaksa menggunakan mock (misal untuk testing) dengan menambahkan env variable:
109
+
110
+ ```env
111
+ NO_REDIS=true
112
+ ```
113
+
114
+ ### Optional: Menggunakan Real Redis dengan Docker
115
+
116
+ Jika Anda ingin menggunakan Redis yang sebenarnya di local environment, kami telah menyertakan konfigurasi `docker-compose.yml` yang aman (menggunakan ACL).
117
+
118
+ 1. Jalankan Redis container:
119
+
120
+ ```bash
121
+ docker-compose up -d
122
+ ```
123
+
124
+ 2. Uncomment konfigurasi Redis di file `.env` Anda:
125
+
126
+ ```env
127
+ REDIS_URL="redis://lapeh:12341234@localhost:6379"
128
+ ```
129
+
130
+ > **Credential Default:**
131
+ >
132
+ > - User: `lapeh`
133
+ > - Password: `12341234`
134
+
78
135
  ## 🛠 Development Tools
79
136
 
80
137
  API Lapeh menyediakan tools untuk mempercepat development, mirip dengan `artisan` di Laravel.
@@ -122,7 +122,22 @@ const selectOption = async (query, options) => {
122
122
  console.log("\n📦 Installing dependencies...");
123
123
  execSync("npm install", { stdio: "inherit", cwd: rootDir });
124
124
 
125
- // 4. Generate JWT Secret
125
+ // 4. Create .vscode/settings.json
126
+ console.log("\n🛠️ Configuring VS Code...");
127
+ const vscodeDir = path.join(rootDir, ".vscode");
128
+ if (!fs.existsSync(vscodeDir)) {
129
+ fs.mkdirSync(vscodeDir, { recursive: true });
130
+ }
131
+ const settingsFile = path.join(vscodeDir, "settings.json");
132
+ const settingsContent = {
133
+ "files.associations": {
134
+ "*.model": "prisma"
135
+ }
136
+ };
137
+ fs.writeFileSync(settingsFile, JSON.stringify(settingsContent, null, 2));
138
+ console.log("✅ VS Code configured (.model support added).");
139
+
140
+ // 5. Generate JWT Secret
126
141
  console.log("\n🔑 Generating JWT Secret...");
127
142
  try {
128
143
  execSync("node scripts/generate-jwt-secret.js", {
package/src/redis.ts CHANGED
@@ -38,12 +38,10 @@ redis.on("error", (err) => {
38
38
  // Replace the global redis instance with mock
39
39
  // Note: This is a runtime switch. Existing listeners might be lost if we don't handle carefully.
40
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
41
+ // Better approach: Since we exported 'redis' as a const (reference), we can't reassign it easily
43
42
  // if other modules already imported it.
44
43
  // BUT, ioredis instance itself is an EventEmitter.
45
-
46
- // Strategy: We keep 'redis' as the main interface.
44
+ // Strategy: We keep 'redis' as the main interface.
47
45
  // If real redis fails, we just don't set isRedisConnected to true for the *real* one.
48
46
  // But wait, the user wants 'bundle redis'.
49
47
  // The best way is to detect failure during init and SWAP the implementation.
@@ -63,6 +61,7 @@ let activeRedis = redis; // Start with real redis attempt
63
61
  export async function initRedis() {
64
62
  if (process.env.NO_REDIS === "true") {
65
63
  activeRedis = mockRedis;
64
+ console.log("✅ Redis: Active (Source: Zero-Config Redis [NO_REDIS=true])");
66
65
  if (process.env.NODE_ENV === "production") {
67
66
  console.warn(
68
67
  "⚠️ WARNING: Running in PRODUCTION with in-memory Redis mock. Data will be lost on restart and not shared between instances."
@@ -75,9 +74,18 @@ export async function initRedis() {
75
74
  await redis.connect();
76
75
  activeRedis = redis; // Keep using real redis
77
76
  isRedisConnected = true;
77
+
78
+ // Determine source label
79
+ const sourceLabel = process.env.REDIS_URL
80
+ ? redisUrl
81
+ : "Zero-Config Redis (Localhost)";
82
+
83
+ console.log(`✅ Redis: Active (Source: ${sourceLabel})`);
78
84
  } catch (err) {
79
85
  // Connection failed, switch to mock
80
- // console.log("Redis failed, using in-memory mock");
86
+ console.log(
87
+ `⚠️ Redis: Connection failed to ${redisUrl}, switching to fallback (Source: Zero-Config Redis [Mock])`
88
+ );
81
89
  activeRedis = mockRedis;
82
90
  isRedisConnected = true; // Mock is always "connected"
83
91
  if (process.env.NODE_ENV === "production") {
@@ -118,4 +126,4 @@ export async function delCache(key: string) {
118
126
  }
119
127
 
120
128
  // Export the proxy as 'redis' so consumers use it transparently
121
- export { redisProxy as redis };
129
+ export { redisProxy as redis };