create-forgeapi 1.0.0
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/README.md +79 -0
- package/dist/bin/index.d.ts +2 -0
- package/dist/bin/index.js +286 -0
- package/package.json +37 -0
- package/template/.env.example +7 -0
- package/template/nodemon.json +7 -0
- package/template/package.json +21 -0
- package/template/src/App.ts +1 -0
- package/template/src/api/controllers/UserController.ts +54 -0
- package/template/src/api/middlewares/AuthMiddleware.ts +24 -0
- package/template/src/base/BaseRepository.ts +112 -0
- package/template/src/base/BaseResponse.ts +45 -0
- package/template/src/base/BaseResponseError.ts +12 -0
- package/template/src/base/BaseSchema.ts +82 -0
- package/template/src/config/DateTimeConfig.ts +4 -0
- package/template/src/config/GlobalHandlerError.ts +81 -0
- package/template/src/config/LoggerConfig.ts +157 -0
- package/template/src/config/LookupConfig.ts +68 -0
- package/template/src/config/MetaDataConfig.ts +25 -0
- package/template/src/config/RouteConfig.ts +98 -0
- package/template/src/config/SwaggerConfig.ts +147 -0
- package/template/src/config/envConfig.ts +5 -0
- package/template/src/core/Server.ts +75 -0
- package/template/src/core/exceptions/BadRequestException.ts +7 -0
- package/template/src/core/exceptions/ForbiddenException.ts +7 -0
- package/template/src/core/exceptions/NotFoundException.ts +7 -0
- package/template/src/core/exceptions/UnauthorizedException.ts +7 -0
- package/template/src/core/repositories/UserRepository.ts +53 -0
- package/template/src/database/DatabaseConnection.ts +11 -0
- package/template/src/database/builder/CustomBuilder.ts +177 -0
- package/template/src/database/builder/CustomFilter.ts +12 -0
- package/template/src/database/builder/MultipleSearchCriteria.ts +25 -0
- package/template/src/database/builder/SearchCriteria.ts +79 -0
- package/template/src/database/entity/ProfileModel.ts +54 -0
- package/template/src/database/entity/UserModel.ts +46 -0
- package/template/src/shared/decorators/ApiDecorator.ts +88 -0
- package/template/src/shared/decorators/LookupDecorator.ts +51 -0
- package/template/src/shared/decorators/MethodeDecorator.ts +38 -0
- package/template/src/shared/helpers/VirtualHelper.ts +53 -0
- package/template/src/shared/interfaces/BaseInterface.ts +14 -0
- package/template/src/shared/interfaces/DatabaseInterface.ts +41 -0
- package/template/src/shared/interfaces/HttpInterface.ts +37 -0
- package/template/src/shared/interfaces/LoggerOptionsInterface.ts +16 -0
- package/template/src/shared/interfaces/MiddlewareInterface.ts +14 -0
- package/template/src/shared/interfaces/RouteMetadataInterface.ts +10 -0
- package/template/src/shared/interfaces/SoftDeleteInterface.ts +8 -0
- package/template/src/shared/interfaces/SwaggerMetadataInterface.ts +12 -0
- package/template/src/shared/interfaces/UserInterface.ts +37 -0
- package/template/src/shared/interfaces/user/UserCreateInterface.ts +4 -0
- package/template/src/shared/models/enum/MiddlewareEnum.ts +19 -0
- package/template/src/shared/models/request/UserCreateRequest.ts +12 -0
- package/template/src/shared/models/response/ApiResponse.ts +27 -0
- package/template/src/shared/types/express.d.ts +14 -0
- package/template/src/shared/types/mongoose.d.ts +7 -0
- package/template/src/shared/utils/ColorUtil.ts +17 -0
- package/template/src/shared/utils/DatabaseUtil.ts +3 -0
- package/template/src/shared/utils/DecoratorUtil.ts +4 -0
- package/template/src/shared/utils/FolderUtil.ts +15 -0
- package/template/src/shared/utils/JwtUtil.ts +21 -0
- package/template/src/shared/utils/SwaggerUtil.ts +107 -0
- package/template/src/shared/utils/TimeUtil.ts +74 -0
- package/template/tsconfig.dev.json +9 -0
- package/template/tsconfig.json +33 -0
- package/template/tsconfig.prod.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# create-forgeapi
|
|
2
|
+
|
|
3
|
+
> 🔨 CLI scaffold untuk **ForgeAPI** — base project Express + TypeScript + MongoDB + Zod + Swagger
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Via npm (jika di-publish)
|
|
8
|
+
```bash
|
|
9
|
+
npx create-forgeapi my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Via GitHub (tanpa publish ke npm) ✅
|
|
13
|
+
```bash
|
|
14
|
+
npx github:username/create-forgeapi my-project
|
|
15
|
+
```
|
|
16
|
+
> Ganti `username` dengan GitHub username kamu. Bisa dipakai dari PC manapun tanpa perlu publish ke npm.
|
|
17
|
+
|
|
18
|
+
### Opsi tambahan
|
|
19
|
+
```bash
|
|
20
|
+
# Skip semua prompt, pakai nilai default
|
|
21
|
+
npx create-forgeapi my-project --yes
|
|
22
|
+
|
|
23
|
+
# Skip cek versi terbaru (mode offline)
|
|
24
|
+
npx create-forgeapi my-project --no-fetch
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Fitur CLI
|
|
28
|
+
|
|
29
|
+
- ✅ **Auto-fetch versi terbaru** semua dependency dari npm registry saat generate
|
|
30
|
+
- 🔔 Notifikasi package mana saja yang ada update
|
|
31
|
+
- 🌿 `git init` + initial commit otomatis
|
|
32
|
+
- 📦 `npm install` otomatis (opsional)
|
|
33
|
+
- 🔒 `.env` dibuat otomatis dari `.env.example`
|
|
34
|
+
|
|
35
|
+
## Yang didapat
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
my-project/
|
|
39
|
+
├── src/
|
|
40
|
+
│ ├── api/ # Controllers & Middlewares
|
|
41
|
+
│ ├── base/ # BaseRepository, BaseResponse, BaseSchema
|
|
42
|
+
│ ├── config/ # Route, Swagger, Logger, ENV config
|
|
43
|
+
│ ├── core/ # Server, Exceptions, Repositories, Services
|
|
44
|
+
│ ├── database/ # MongoDB connection, Builder, Entities
|
|
45
|
+
│ └── shared/ # Decorators, Interfaces, Models, Utils
|
|
46
|
+
├── .env # Konfigurasi environment (auto-generated)
|
|
47
|
+
├── .env.example
|
|
48
|
+
├── nodemon.json
|
|
49
|
+
├── tsconfig.json
|
|
50
|
+
└── package.json # Versi dependency selalu terbaru
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Stack
|
|
54
|
+
|
|
55
|
+
| | |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Framework | Express 5 |
|
|
58
|
+
| Language | TypeScript 5 |
|
|
59
|
+
| Database | MongoDB (Mongoose) |
|
|
60
|
+
| Validation | Zod |
|
|
61
|
+
| Auth | JWT |
|
|
62
|
+
| Docs | Swagger UI |
|
|
63
|
+
| Logger | Custom (file + console) |
|
|
64
|
+
|
|
65
|
+
## Next steps setelah scaffold
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd my-project
|
|
69
|
+
# Edit .env — isi MONGO_DB_URL dan JWT_SECRET
|
|
70
|
+
npm run dev
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Swagger tersedia di `http://localhost:3000/api-docs`
|
|
74
|
+
|
|
75
|
+
## Cara update template
|
|
76
|
+
|
|
77
|
+
Cukup update file di folder `template/` atau `bin/index.ts` di repo GitHub ini,
|
|
78
|
+
lalu semua orang yang menjalankan `npx github:username/create-forgeapi` akan
|
|
79
|
+
otomatis mendapat versi terbaru.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const readline = __importStar(require("readline"));
|
|
40
|
+
const https = __importStar(require("https"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
// ─── Colors ───────────────────────────────────────────────────────────────────
|
|
43
|
+
const c = {
|
|
44
|
+
reset: "\x1b[0m",
|
|
45
|
+
bright: "\x1b[1m",
|
|
46
|
+
green: "\x1b[32m",
|
|
47
|
+
yellow: "\x1b[33m",
|
|
48
|
+
cyan: "\x1b[36m",
|
|
49
|
+
magenta: "\x1b[35m",
|
|
50
|
+
red: "\x1b[31m",
|
|
51
|
+
gray: "\x1b[90m",
|
|
52
|
+
};
|
|
53
|
+
const log = {
|
|
54
|
+
info: (m) => console.log(`${c.cyan}ℹ ${c.reset}${m}`),
|
|
55
|
+
success: (m) => console.log(`${c.green}✅ ${c.reset}${m}`),
|
|
56
|
+
warn: (m) => console.log(`${c.yellow}⚠️ ${c.reset}${m}`),
|
|
57
|
+
error: (m) => console.log(`${c.red}❌ ${c.reset}${m}`),
|
|
58
|
+
step: (m) => console.log(`${c.magenta}▶ ${c.bright}${m}${c.reset}`),
|
|
59
|
+
dim: (m) => console.log(`${c.gray}${m}${c.reset}`),
|
|
60
|
+
};
|
|
61
|
+
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
62
|
+
function banner() {
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(`${c.cyan}${c.bright} ███████╗ ██████╗ ██████╗ ██████╗ ███████╗ █████╗ ██████╗ ██╗${c.reset}`);
|
|
65
|
+
console.log(`${c.cyan}${c.bright} ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝██╔══██╗██╔══██╗██║${c.reset}`);
|
|
66
|
+
console.log(`${c.cyan}${c.bright} █████╗ ██║ ██║██████╔╝██║ ███╗█████╗ ███████║██████╔╝██║${c.reset}`);
|
|
67
|
+
console.log(`${c.cyan}${c.bright} ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██║██╔═══╝ ██║${c.reset}`);
|
|
68
|
+
console.log(`${c.cyan}${c.bright} ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗██║ ██║██║ ██║${c.reset}`);
|
|
69
|
+
console.log(`${c.cyan}${c.bright} ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝${c.reset}`);
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(`${c.gray} Express + TypeScript + MongoDB + Zod + Swagger${c.reset}`);
|
|
72
|
+
console.log(`${c.gray} Base API scaffold by create-forgeapi${c.reset}`);
|
|
73
|
+
console.log();
|
|
74
|
+
}
|
|
75
|
+
// ─── Prompt helper ────────────────────────────────────────────────────────────
|
|
76
|
+
function prompt(rl, question, defaultVal = "") {
|
|
77
|
+
return new Promise(resolve => {
|
|
78
|
+
const hint = defaultVal ? ` ${c.gray}(${defaultVal})${c.reset}` : "";
|
|
79
|
+
rl.question(`${c.yellow}?${c.reset} ${question}${hint}: `, answer => {
|
|
80
|
+
resolve(answer.trim() || defaultVal);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// ─── Fetch latest version from npm registry ───────────────────────────────────
|
|
85
|
+
function fetchLatestVersion(pkg) {
|
|
86
|
+
return new Promise(resolve => {
|
|
87
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`;
|
|
88
|
+
https.get(url, { headers: { "User-Agent": "create-forgeapi" } }, res => {
|
|
89
|
+
let data = "";
|
|
90
|
+
res.on("data", chunk => data += chunk);
|
|
91
|
+
res.on("end", () => {
|
|
92
|
+
try {
|
|
93
|
+
const json = JSON.parse(data);
|
|
94
|
+
resolve(json.version ?? null);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
resolve(null);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}).on("error", () => resolve(null));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// ─── Spinner ──────────────────────────────────────────────────────────────────
|
|
104
|
+
function spinner(text) {
|
|
105
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
106
|
+
let i = 0;
|
|
107
|
+
const iv = setInterval(() => {
|
|
108
|
+
process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${text}`);
|
|
109
|
+
}, 80);
|
|
110
|
+
return () => {
|
|
111
|
+
clearInterval(iv);
|
|
112
|
+
process.stdout.write("\r\x1b[K"); // clear line
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// ─── Resolve latest versions for all deps ─────────────────────────────────────
|
|
116
|
+
async function resolveLatestVersions(deps) {
|
|
117
|
+
const stop = spinner("Checking latest package versions...");
|
|
118
|
+
const entries = await Promise.all(Object.entries(deps).map(async ([pkg, fallback]) => {
|
|
119
|
+
const latest = await fetchLatestVersion(pkg);
|
|
120
|
+
return [pkg, latest ? `^${latest}` : fallback];
|
|
121
|
+
}));
|
|
122
|
+
stop();
|
|
123
|
+
return Object.fromEntries(entries);
|
|
124
|
+
}
|
|
125
|
+
// ─── Copy template recursively ────────────────────────────────────────────────
|
|
126
|
+
function copyDir(src, dest, replacements) {
|
|
127
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
128
|
+
for (const entry of fs.readdirSync(src)) {
|
|
129
|
+
const srcPath = path.join(src, entry);
|
|
130
|
+
const destPath = path.join(dest, entry);
|
|
131
|
+
const stat = fs.statSync(srcPath);
|
|
132
|
+
if (stat.isDirectory()) {
|
|
133
|
+
copyDir(srcPath, destPath, replacements);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
137
|
+
if (entry === "package.json") {
|
|
138
|
+
for (const [key, val] of Object.entries(replacements)) {
|
|
139
|
+
content = content.replaceAll(key, val);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(destPath, content);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
147
|
+
async function main() {
|
|
148
|
+
banner();
|
|
149
|
+
const args = process.argv.slice(2);
|
|
150
|
+
const yesFlag = args.includes("--yes") || args.includes("-y");
|
|
151
|
+
const skipFetch = args.includes("--no-fetch");
|
|
152
|
+
let projectName = args.find(a => !a.startsWith("-")) || "";
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
154
|
+
if (!projectName) {
|
|
155
|
+
projectName = yesFlag ? "my-api" : await prompt(rl, "Project name", "my-api");
|
|
156
|
+
}
|
|
157
|
+
const description = yesFlag ? "REST API built with ForgeAPI" : await prompt(rl, "Description", "REST API built with ForgeAPI");
|
|
158
|
+
const author = yesFlag ? "" : await prompt(rl, "Author", "");
|
|
159
|
+
const installDeps = yesFlag ? "y" : await prompt(rl, "Install dependencies now? (y/n)", "y");
|
|
160
|
+
rl.close();
|
|
161
|
+
// Validasi nama
|
|
162
|
+
if (!/^[a-z0-9-_]+$/.test(projectName)) {
|
|
163
|
+
log.error("Project name hanya boleh huruf kecil, angka, - dan _");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
167
|
+
if (fs.existsSync(targetDir)) {
|
|
168
|
+
log.error(`Folder "${projectName}" sudah ada!`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
console.log();
|
|
172
|
+
log.step(`Creating project: ${c.cyan}${projectName}${c.reset}`);
|
|
173
|
+
// ── Resolve latest versions ────────────────────────────────────────────────
|
|
174
|
+
// Daftar semua package yang perlu dicek versi terbarunya
|
|
175
|
+
const depPackages = {
|
|
176
|
+
"cors": "^2.8.6",
|
|
177
|
+
"dotenv": "^17.2.3",
|
|
178
|
+
"express": "^5.2.1",
|
|
179
|
+
"express-rate-limit": "^8.2.1",
|
|
180
|
+
"jsonwebtoken": "^9.0.3",
|
|
181
|
+
"mongoose": "^8.0.0",
|
|
182
|
+
"reflect-metadata": "^0.2.2",
|
|
183
|
+
"swagger-ui-express": "^5.0.1",
|
|
184
|
+
"zod": "^4.3.6",
|
|
185
|
+
};
|
|
186
|
+
const devPackages = {
|
|
187
|
+
"@types/cors": "^2.8.19",
|
|
188
|
+
"@types/express": "^5.0.6",
|
|
189
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
190
|
+
"@types/node": "^25.0.3",
|
|
191
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
192
|
+
"cross-env": "^10.1.0",
|
|
193
|
+
"nodemon": "^3.1.11",
|
|
194
|
+
"rimraf": "^6.1.2",
|
|
195
|
+
"ts-node": "^10.9.2",
|
|
196
|
+
"tsc-alias": "^1.8.16",
|
|
197
|
+
"tsconfig-paths": "^4.2.0",
|
|
198
|
+
"typescript": "^5.9.3",
|
|
199
|
+
};
|
|
200
|
+
let resolvedDeps = depPackages;
|
|
201
|
+
let resolvedDevDeps = devPackages;
|
|
202
|
+
if (!skipFetch) {
|
|
203
|
+
[resolvedDeps, resolvedDevDeps] = await Promise.all([
|
|
204
|
+
resolveLatestVersions(depPackages),
|
|
205
|
+
resolveLatestVersions(devPackages),
|
|
206
|
+
]);
|
|
207
|
+
log.success("Package versions resolved");
|
|
208
|
+
// Tampilkan yang ada update
|
|
209
|
+
const allOld = { ...depPackages, ...devPackages };
|
|
210
|
+
const allNew = { ...resolvedDeps, ...resolvedDevDeps };
|
|
211
|
+
const updated = Object.entries(allNew).filter(([pkg, ver]) => ver !== allOld[pkg]);
|
|
212
|
+
if (updated.length > 0) {
|
|
213
|
+
log.info(`${updated.length} package(s) updated to latest:`);
|
|
214
|
+
updated.forEach(([pkg, ver]) => {
|
|
215
|
+
const old = allOld[pkg];
|
|
216
|
+
log.dim(` ${pkg}: ${c.yellow}${old}${c.reset} → ${c.green}${ver}${c.reset}`);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
log.info("All packages are already at latest versions");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
log.warn("Skipping version check (--no-fetch)");
|
|
225
|
+
}
|
|
226
|
+
// ── Inject versi ke template package.json ─────────────────────────────────
|
|
227
|
+
// Serialize versi ke format yang bisa di-replace di template
|
|
228
|
+
const depsJson = JSON.stringify(resolvedDeps, null, 4).slice(2, -1).trim(); // isi object
|
|
229
|
+
const devDepsJson = JSON.stringify(resolvedDevDeps, null, 4).slice(2, -1).trim();
|
|
230
|
+
// ── Copy template ──────────────────────────────────────────────────────────
|
|
231
|
+
// __dirname saat build = dist/bin, naik 2 level ke root package
|
|
232
|
+
const templateDir = path.join(__dirname, "..", "..", "template");
|
|
233
|
+
copyDir(templateDir, targetDir, {
|
|
234
|
+
"{{PROJECT_NAME}}": projectName,
|
|
235
|
+
"{{PROJECT_DESCRIPTION}}": description,
|
|
236
|
+
"{{AUTHOR}}": author,
|
|
237
|
+
"{{DEPENDENCIES}}": depsJson,
|
|
238
|
+
"{{DEV_DEPENDENCIES}}": devDepsJson,
|
|
239
|
+
});
|
|
240
|
+
log.success("Template copied");
|
|
241
|
+
// ── Rename .env.example → .env ─────────────────────────────────────────────
|
|
242
|
+
const envFile = path.join(targetDir, ".env");
|
|
243
|
+
const envExample = path.join(targetDir, ".env.example");
|
|
244
|
+
if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
|
|
245
|
+
fs.copyFileSync(envExample, envFile);
|
|
246
|
+
}
|
|
247
|
+
// ── Init git ───────────────────────────────────────────────────────────────
|
|
248
|
+
try {
|
|
249
|
+
(0, child_process_1.execSync)("git init", { cwd: targetDir, stdio: "ignore" });
|
|
250
|
+
(0, child_process_1.execSync)("git add .", { cwd: targetDir, stdio: "ignore" });
|
|
251
|
+
(0, child_process_1.execSync)('git commit -m "chore: init project from create-forgeapi"', { cwd: targetDir, stdio: "ignore" });
|
|
252
|
+
log.success("Git initialized");
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
log.warn("Git not found, skipping git init");
|
|
256
|
+
}
|
|
257
|
+
// ── Install deps ───────────────────────────────────────────────────────────
|
|
258
|
+
if (installDeps.toLowerCase() === "y") {
|
|
259
|
+
log.step("Installing dependencies...");
|
|
260
|
+
try {
|
|
261
|
+
(0, child_process_1.execSync)("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
262
|
+
log.success("Dependencies installed");
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
log.error(`npm install failed. Run manually: cd ${projectName} && npm install`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// ── Done ───────────────────────────────────────────────────────────────────
|
|
269
|
+
console.log();
|
|
270
|
+
console.log(`${c.green}${c.bright} 🎉 Project ready!${c.reset}`);
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(` ${c.gray}Next steps:${c.reset}`);
|
|
273
|
+
console.log(` ${c.cyan}cd ${projectName}${c.reset}`);
|
|
274
|
+
if (installDeps.toLowerCase() !== "y") {
|
|
275
|
+
console.log(` ${c.cyan}npm install${c.reset}`);
|
|
276
|
+
}
|
|
277
|
+
console.log(` ${c.cyan}# edit .env — isi MONGO_DB_URL dan JWT_SECRET${c.reset}`);
|
|
278
|
+
console.log(` ${c.cyan}npm run dev${c.reset}`);
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(` ${c.gray}Swagger docs:${c.reset} ${c.cyan}http://localhost:3000/api-docs${c.reset}`);
|
|
281
|
+
console.log();
|
|
282
|
+
}
|
|
283
|
+
main().catch(err => {
|
|
284
|
+
console.error(err);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-forgeapi",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI scaffold for ForgeAPI — Express + TypeScript + MongoDB + Zod + Swagger base project",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"express",
|
|
7
|
+
"typescript",
|
|
8
|
+
"mongodb",
|
|
9
|
+
"zod",
|
|
10
|
+
"swagger",
|
|
11
|
+
"scaffold",
|
|
12
|
+
"boilerplate",
|
|
13
|
+
"starter",
|
|
14
|
+
"forgeapi"
|
|
15
|
+
],
|
|
16
|
+
"author": "Alis-Dev",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"main": "dist/bin/index.js",
|
|
19
|
+
"bin": {
|
|
20
|
+
"create-forgeapi": "dist/bin/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"template"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"dev": "ts-node bin/index.ts",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.0.3",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"ts-node": "^10.9.2"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "{{PROJECT_DESCRIPTION}}",
|
|
5
|
+
"author": "{{AUTHOR}}",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"main": "dist/App.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "cross-env NODE_ENV=dev nodemon",
|
|
10
|
+
"build": "tsc && tsc-alias",
|
|
11
|
+
"build:prod": "tsc -p tsconfig.prod.json && tsc-alias",
|
|
12
|
+
"start": "node dist/App.js",
|
|
13
|
+
"clean": "rimraf dist"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
{{DEPENDENCIES}}
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
{{DEV_DEPENDENCIES}}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@core/Server";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {Request, Response} from 'express';
|
|
2
|
+
import {ApiOperation, BodyResponse, Controller, RequiredBody, SwaggerTag} from "@shared/decorators/ApiDecorator";
|
|
3
|
+
import {PostMapping} from "@shared/decorators/MethodeDecorator";
|
|
4
|
+
import {UserCreateRequest} from "@shared/models/request/UserCreateRequest";
|
|
5
|
+
import {SuccessResponse} from "@shared/models/response/ApiResponse";
|
|
6
|
+
import BaseResponse from "@base/BaseResponse";
|
|
7
|
+
import UserRepository from "@core/repositories/UserRepository";
|
|
8
|
+
import BadRequestException from "@core/exceptions/BadRequestException";
|
|
9
|
+
import {MiddlewareCodeAccess} from "@shared/models/enum/MiddlewareEnum";
|
|
10
|
+
|
|
11
|
+
@Controller("/api/user")
|
|
12
|
+
@SwaggerTag("User")
|
|
13
|
+
export default class UserController {
|
|
14
|
+
|
|
15
|
+
private userRepository = new UserRepository();
|
|
16
|
+
|
|
17
|
+
@PostMapping("/create", {authenticate: true, codeAccess: MiddlewareCodeAccess.TAMBAH})
|
|
18
|
+
@ApiOperation("Create User", "Membuat user baru")
|
|
19
|
+
@RequiredBody(UserCreateRequest)
|
|
20
|
+
@BodyResponse(201, SuccessResponse(UserCreateRequest), "User berhasil dibuat")
|
|
21
|
+
async create(req: Request, res: Response) {
|
|
22
|
+
// 1. Validasi input dengan Zod
|
|
23
|
+
const parsed = UserCreateRequest.safeParse(req.body);
|
|
24
|
+
if (!parsed.success) {
|
|
25
|
+
throw new BadRequestException(
|
|
26
|
+
"Validasi gagal",
|
|
27
|
+
parsed.error.issues.map(e => ({ field: e.path.map(String).join('.'), message: e.message }))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Panggil BaseResponse.ok → jalankan service/repository di dalam callback
|
|
32
|
+
return BaseResponse.ok(
|
|
33
|
+
res,
|
|
34
|
+
() => this.userRepository.createWithProfile(
|
|
35
|
+
{
|
|
36
|
+
username: parsed.data.username,
|
|
37
|
+
email: parsed.data.email,
|
|
38
|
+
password: parsed.data.password,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
firstName: parsed.data.firstName,
|
|
42
|
+
lastName: parsed.data.lastName,
|
|
43
|
+
phone: parsed.data.phone,
|
|
44
|
+
address: parsed.data.address,
|
|
45
|
+
dateOfBirth: parsed.data.dateOfBirth,
|
|
46
|
+
}
|
|
47
|
+
),
|
|
48
|
+
"User berhasil dibuat"
|
|
49
|
+
);
|
|
50
|
+
// BaseResponse.ok akan otomatis return:
|
|
51
|
+
// { success: true, message: "User berhasil dibuat", data: <hasil createWithProfile> }
|
|
52
|
+
// Jika callback throw exception → ditangkap GlobalHandlerError → return ErrorResponse
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Response, Request, NextFunction} from "express";
|
|
2
|
+
import UnauthorizedException from "@core/exceptions/UnauthorizedException";
|
|
3
|
+
import JwtUtil from "@shared/utils/JwtUtil";
|
|
4
|
+
import {log} from "@config/LoggerConfig";
|
|
5
|
+
|
|
6
|
+
export default async (
|
|
7
|
+
req: Request,
|
|
8
|
+
res: Response,
|
|
9
|
+
next: NextFunction
|
|
10
|
+
) => {
|
|
11
|
+
try {
|
|
12
|
+
const authHeader = req.headers.authorization;
|
|
13
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
14
|
+
return next(new UnauthorizedException("Authorization header missing or malformed"));
|
|
15
|
+
}
|
|
16
|
+
log.info("ACCESS: ", req.authenticate?.codeAccess);
|
|
17
|
+
const token = authHeader.substring(7);
|
|
18
|
+
const decoded = await JwtUtil.validateToken(token);
|
|
19
|
+
(req as any).user = decoded;
|
|
20
|
+
next();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
next(error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Document,
|
|
3
|
+
Model
|
|
4
|
+
} from "mongoose";
|
|
5
|
+
import {
|
|
6
|
+
Pageable,
|
|
7
|
+
PageResult,
|
|
8
|
+
SortOption
|
|
9
|
+
} from "@shared/interfaces/DatabaseInterface";
|
|
10
|
+
import CustomBuilder from "@database/builder/CustomBuilder";
|
|
11
|
+
import UserEntity from "@database/entity/UserModel";
|
|
12
|
+
|
|
13
|
+
export default class BaseRepository <T extends Document> {
|
|
14
|
+
constructor(protected model: Model<T>) {}
|
|
15
|
+
|
|
16
|
+
async findAll(builder: CustomBuilder<T>, pageable: Pageable): Promise<PageResult<T>> {
|
|
17
|
+
const { page, size, sort } = pageable;
|
|
18
|
+
const skip = (page - 1) * size;
|
|
19
|
+
|
|
20
|
+
if (builder.hasLookups()) {
|
|
21
|
+
return this.findAllWithAggregation(builder, pageable);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const query = builder.build();
|
|
25
|
+
const [content, totalElements] = await Promise.all([
|
|
26
|
+
this.model
|
|
27
|
+
.find(query)
|
|
28
|
+
.sort(sort || { _id: -1 })
|
|
29
|
+
.skip(skip)
|
|
30
|
+
.limit(size)
|
|
31
|
+
.exec(),
|
|
32
|
+
this.model.countDocuments(query).exec()
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const totalPages = Math.ceil(totalElements / size);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
content,
|
|
39
|
+
totalElements,
|
|
40
|
+
totalPages,
|
|
41
|
+
currentPage: page,
|
|
42
|
+
size,
|
|
43
|
+
hasNext: page < totalPages,
|
|
44
|
+
hasPrevious: page > 1
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async list(builder: CustomBuilder<T>, sort?: SortOption): Promise<T[]> {
|
|
49
|
+
if (builder.hasLookups()) {
|
|
50
|
+
const pipeline = builder.buildAggregation();
|
|
51
|
+
|
|
52
|
+
if (sort) {
|
|
53
|
+
pipeline.push({ $sort: sort });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this.model.aggregate(pipeline).exec();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const query = builder.build();
|
|
60
|
+
return this.model
|
|
61
|
+
.find(query)
|
|
62
|
+
.sort(sort || { _id: -1 })
|
|
63
|
+
.exec();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async findOne(builder: CustomBuilder<T>): Promise<T | null> {
|
|
67
|
+
if (builder.hasLookups()) {
|
|
68
|
+
const pipeline = builder.buildAggregation();
|
|
69
|
+
pipeline.push({ $limit: 1 });
|
|
70
|
+
|
|
71
|
+
const result = await this.model.aggregate(pipeline).exec();
|
|
72
|
+
return result.length > 0 ? result[0] : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const query = builder.build();
|
|
76
|
+
return this.model.findOne(query).exec();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async findAllWithAggregation(
|
|
80
|
+
builder: CustomBuilder<T>,
|
|
81
|
+
pageable: Pageable
|
|
82
|
+
): Promise<PageResult<T>> {
|
|
83
|
+
const { page, size, sort } = pageable;
|
|
84
|
+
const skip = (page - 1) * size;
|
|
85
|
+
|
|
86
|
+
const pipeline = builder.buildAggregation();
|
|
87
|
+
|
|
88
|
+
if (sort) {
|
|
89
|
+
pipeline.push({ $sort: sort });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const countPipeline = [...pipeline, { $count: 'total' }];
|
|
93
|
+
const countResult = await this.model.aggregate(countPipeline).exec();
|
|
94
|
+
const totalElements = countResult.length > 0 ? countResult[0].total : 0;
|
|
95
|
+
|
|
96
|
+
pipeline.push({ $skip: skip });
|
|
97
|
+
pipeline.push({ $limit: size });
|
|
98
|
+
|
|
99
|
+
const content = await this.model.aggregate(pipeline).exec();
|
|
100
|
+
const totalPages = Math.ceil(totalElements / size);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
content: content as any,
|
|
104
|
+
totalElements,
|
|
105
|
+
totalPages,
|
|
106
|
+
currentPage: page,
|
|
107
|
+
size,
|
|
108
|
+
hasNext: page < totalPages,
|
|
109
|
+
hasPrevious: page > 1
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|