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.
Files changed (64) hide show
  1. package/README.md +79 -0
  2. package/dist/bin/index.d.ts +2 -0
  3. package/dist/bin/index.js +286 -0
  4. package/package.json +37 -0
  5. package/template/.env.example +7 -0
  6. package/template/nodemon.json +7 -0
  7. package/template/package.json +21 -0
  8. package/template/src/App.ts +1 -0
  9. package/template/src/api/controllers/UserController.ts +54 -0
  10. package/template/src/api/middlewares/AuthMiddleware.ts +24 -0
  11. package/template/src/base/BaseRepository.ts +112 -0
  12. package/template/src/base/BaseResponse.ts +45 -0
  13. package/template/src/base/BaseResponseError.ts +12 -0
  14. package/template/src/base/BaseSchema.ts +82 -0
  15. package/template/src/config/DateTimeConfig.ts +4 -0
  16. package/template/src/config/GlobalHandlerError.ts +81 -0
  17. package/template/src/config/LoggerConfig.ts +157 -0
  18. package/template/src/config/LookupConfig.ts +68 -0
  19. package/template/src/config/MetaDataConfig.ts +25 -0
  20. package/template/src/config/RouteConfig.ts +98 -0
  21. package/template/src/config/SwaggerConfig.ts +147 -0
  22. package/template/src/config/envConfig.ts +5 -0
  23. package/template/src/core/Server.ts +75 -0
  24. package/template/src/core/exceptions/BadRequestException.ts +7 -0
  25. package/template/src/core/exceptions/ForbiddenException.ts +7 -0
  26. package/template/src/core/exceptions/NotFoundException.ts +7 -0
  27. package/template/src/core/exceptions/UnauthorizedException.ts +7 -0
  28. package/template/src/core/repositories/UserRepository.ts +53 -0
  29. package/template/src/database/DatabaseConnection.ts +11 -0
  30. package/template/src/database/builder/CustomBuilder.ts +177 -0
  31. package/template/src/database/builder/CustomFilter.ts +12 -0
  32. package/template/src/database/builder/MultipleSearchCriteria.ts +25 -0
  33. package/template/src/database/builder/SearchCriteria.ts +79 -0
  34. package/template/src/database/entity/ProfileModel.ts +54 -0
  35. package/template/src/database/entity/UserModel.ts +46 -0
  36. package/template/src/shared/decorators/ApiDecorator.ts +88 -0
  37. package/template/src/shared/decorators/LookupDecorator.ts +51 -0
  38. package/template/src/shared/decorators/MethodeDecorator.ts +38 -0
  39. package/template/src/shared/helpers/VirtualHelper.ts +53 -0
  40. package/template/src/shared/interfaces/BaseInterface.ts +14 -0
  41. package/template/src/shared/interfaces/DatabaseInterface.ts +41 -0
  42. package/template/src/shared/interfaces/HttpInterface.ts +37 -0
  43. package/template/src/shared/interfaces/LoggerOptionsInterface.ts +16 -0
  44. package/template/src/shared/interfaces/MiddlewareInterface.ts +14 -0
  45. package/template/src/shared/interfaces/RouteMetadataInterface.ts +10 -0
  46. package/template/src/shared/interfaces/SoftDeleteInterface.ts +8 -0
  47. package/template/src/shared/interfaces/SwaggerMetadataInterface.ts +12 -0
  48. package/template/src/shared/interfaces/UserInterface.ts +37 -0
  49. package/template/src/shared/interfaces/user/UserCreateInterface.ts +4 -0
  50. package/template/src/shared/models/enum/MiddlewareEnum.ts +19 -0
  51. package/template/src/shared/models/request/UserCreateRequest.ts +12 -0
  52. package/template/src/shared/models/response/ApiResponse.ts +27 -0
  53. package/template/src/shared/types/express.d.ts +14 -0
  54. package/template/src/shared/types/mongoose.d.ts +7 -0
  55. package/template/src/shared/utils/ColorUtil.ts +17 -0
  56. package/template/src/shared/utils/DatabaseUtil.ts +3 -0
  57. package/template/src/shared/utils/DecoratorUtil.ts +4 -0
  58. package/template/src/shared/utils/FolderUtil.ts +15 -0
  59. package/template/src/shared/utils/JwtUtil.ts +21 -0
  60. package/template/src/shared/utils/SwaggerUtil.ts +107 -0
  61. package/template/src/shared/utils/TimeUtil.ts +74 -0
  62. package/template/tsconfig.dev.json +9 -0
  63. package/template/tsconfig.json +33 -0
  64. 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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,7 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+ MONGO_DB_URL=mongodb://localhost:27017/mydb
4
+ MONGO_DB_USER=
5
+ MONGO_DB_PASSWORD=
6
+ JWT_SECRET=your_jwt_secret_here
7
+ JWT_EXPIRES_IN=7d
@@ -0,0 +1,7 @@
1
+ {
2
+ "watch": ["src"],
3
+ "ext": "ts,json",
4
+ "ignore": ["src/**/*.spec.ts"],
5
+ "exec": "ts-node --project tsconfig.json -r tsconfig-paths/register src/App.ts"
6
+ }
7
+
@@ -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
+ }