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 +5 -6
- package/bin/index.js +292 -190
- package/docker-compose.yml +8 -1
- package/package.json +1 -1
- package/readme.md +57 -0
- package/scripts/init-project.js +16 -1
- package/src/redis.ts +14 -6
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
52
|
+
fs.copyFileSync(src, dest);
|
|
88
53
|
}
|
|
89
54
|
}
|
|
90
55
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
const destPath = path.join(dest, entry.name);
|
|
89
|
+
// Update Lapeh version tag
|
|
90
|
+
currentPackageJson.dependencies["lapeh"] = templatePackageJson.version;
|
|
111
91
|
|
|
112
|
-
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
92
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(currentPackageJson, null, 2));
|
|
115
93
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
119
|
+
const currentDir = process.cwd();
|
|
120
|
+
const projectDir = path.join(currentDir, projectName);
|
|
121
|
+
const templateDir = path.join(__dirname, '..');
|
|
144
122
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
execSync('npx prisma migrate dev --name init_setup', { cwd: projectDir, stdio: 'inherit' });
|
|
270
|
+
fs.writeFileSync(envPath, envContent);
|
|
271
|
+
}
|
|
213
272
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
console.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
}
|
package/docker-compose.yml
CHANGED
|
@@ -2,7 +2,14 @@ version: "3.9"
|
|
|
2
2
|
services:
|
|
3
3
|
redis:
|
|
4
4
|
image: redis:7-alpine
|
|
5
|
-
command:
|
|
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
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.
|
package/scripts/init-project.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 };
|