diff --git a/.gitignore b/.gitignore
index 10df4e8..077407c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,7 @@ src/modules
out
dist
*.tgz
-
+database.sqlite
# code coverage
coverage
*.lcov
diff --git a/README.md b/README.md
index e935655..34894c9 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
- A Lean Framework for Enterprise Scalability.
+ A lightweight, composable framework on Bun.
"Complexity doesn't have to be heavy.
Built on Bun, designed for Logic, optimized for Scale."
@@ -53,7 +53,7 @@
GamanJS currently only supports Bun runtime.
```bash
-bun create gaman@latest
+bun create gaman@latest
```
## GamanJS Project Structure 🏗️
@@ -65,7 +65,11 @@ In a production scale or **Enterprise** project, the GamanJS folder structure wi
```
src/
├── index.ts # The Orchestrator (Entry Point)
-├── database/ # Database Configuration
+├── console/ # Helper tool for adding custom commands
+├── database/ # Database Configuration
+│ ├── models/ # Global model and schema databases
+│ ├── migrations/ # Database migration files
+│ └── seeders/ # Database seeders files
└── modules/ # Powerhouse of your Application
├── app/ # Infrastructure Module (Global/Shared)
│ ├── controllers/ # Handlers for Global Requests (Health, Index)
@@ -79,14 +83,14 @@ src/
│ └── UserController.ts
├── services/ # Business Logic (Auth Logic, User CRUD)
│ └── UserService.ts
- ├── models/ # Data Access Layer (Powered by @gaman/orm)
+ ├── models/ # Data Access Layer (Powered by @gaman/db)
│ └── UserModel.ts
└── UserRouter.ts # Scoped Routes & Feature Middleware
```
## Documentation
-visit our [https://gamanjs.github.io/](https://gamanjs.github.io/)
+visit our [https://gaman.js.org/](https://gaman.js.org/)
## Star History
@@ -101,6 +105,7 @@ visit our [https://gamanjs.github.io/](https://gamanjs.github.io/)
## All contributors ✨
+
## Contributing
diff --git a/bun.lock b/bun.lock
index 8bcdba4..323d279 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,11 +6,16 @@
"name": "gaman",
"devDependencies": {
"@types/bun": "latest",
+ "@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"edge.js": "^6.5.0",
"esbuild-fix-imports-plugin": "^1.0.23",
+ "kysely": "^0.28.16",
+ "kysely-bun-sqlite": "^0.4.0",
+ "mysql2": "^3.22.2",
+ "pg": "^8.20.0",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -218,6 +223,8 @@
"@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="],
+ "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
+
"@types/pluralize": ["@types/pluralize@0.0.33", "", {}, "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
@@ -232,6 +239,8 @@
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
+ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
+
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
@@ -252,6 +261,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"edge-error": ["edge-error@4.0.2", "", {}, "sha512-jB76VYn8wapDHKHSOmP3vbKLoa77RJYsTLNmfl8+cuCD69uxZtP3h+kqV+Prw/YkYmN7yHyp4IApE15pDByk0A=="],
@@ -276,12 +287,22 @@
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+ "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
+
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
+
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-stringify": ["js-stringify@1.0.2", "", {}, "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g=="],
+ "kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="],
+
+ "kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="],
+
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
@@ -312,20 +333,44 @@
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
+ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
+
+ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+ "mysql2": ["mysql2@3.22.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-snC/L6YoCJPFpozZo3p3hiOlt9ItQ7sCnLSziFLlIttEzsPhrdcPT8g21BiQ7Oqif25W4Xq1IFuBzBvoFYDf0Q=="],
+
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
+ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+ "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="],
+
+ "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
+
+ "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="],
+
+ "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
+
+ "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
+
+ "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
+
+ "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
+
+ "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
@@ -340,6 +385,14 @@
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
+ "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
+
+ "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
+
+ "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
+
+ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
+
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -356,6 +409,8 @@
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="],
@@ -364,6 +419,10 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
+
+ "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
+
"stringify-attributes": ["stringify-attributes@4.0.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-6Hq3K153wTTfhEHb4V/viuqmb0DRn08JCrRnmqc4Q/tmoNuvd4DEyqkiiJXtvVz8ZSUhlCQr7zCpCVTgrelesg=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
@@ -392,6 +451,8 @@
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
+ "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
+
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
diff --git a/package.json b/package.json
index 4d579b3..8d4026d 100644
--- a/package.json
+++ b/package.json
@@ -71,16 +71,22 @@
"framework"
],
"scripts": {
+ "test-serv": "KAME_CLI=true bun src-test/index.ts",
"release": "bun build.ts && bun test && npm publish --access public",
"kame": "bun packages/kame/src/index.ts"
},
"devDependencies": {
"@types/bun": "latest",
+ "@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"edge.js": "^6.5.0",
"esbuild-fix-imports-plugin": "^1.0.23",
+ "kysely": "^0.28.16",
+ "kysely-bun-sqlite": "^0.4.0",
+ "mysql2": "^3.22.2",
+ "pg": "^8.20.0",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
diff --git a/packages/db/.npmignore b/packages/db/.npmignore
new file mode 100644
index 0000000..123ec67
--- /dev/null
+++ b/packages/db/.npmignore
@@ -0,0 +1,4 @@
+src
+test
+node_modules
+log
\ No newline at end of file
diff --git a/packages/db/README.md b/packages/db/README.md
new file mode 100644
index 0000000..4b14b01
--- /dev/null
+++ b/packages/db/README.md
@@ -0,0 +1 @@
+# @gaman/db
\ No newline at end of file
diff --git a/packages/db/package.json b/packages/db/package.json
new file mode 100644
index 0000000..7b2e4ef
--- /dev/null
+++ b/packages/db/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@gaman/db",
+ "version": "0.1.0",
+ "author": "angga7togk",
+ "license": "MIT",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs"
+ },
+ "./column": {
+ "types": "./dist/column/index.d.ts",
+ "require": "./dist/column/index.js",
+ "import": "./dist/column/index.mjs"
+ },
+ "./migration": {
+ "types": "./dist/migration/index.d.ts",
+ "require": "./dist/migration/index.js",
+ "import": "./dist/migration/index.mjs"
+ }
+ },
+ "keywords": [
+ "gamanjs",
+ "gaman",
+ "database",
+ "gaman db",
+ "orm",
+ "gaman orm",
+ "bun"
+ ],
+ "repository": {
+ "url": "git+https://github.com/GamanJS/gaman.git"
+ },
+ "bugs": {
+ "url": "https://github.com/GamanJS/gaman/issues"
+ },
+ "scripts": {
+ "build": "tsc -b -v",
+ "clean": "tsc -b --clean"
+ },
+ "homepage": "https://github.com/GamanJS/gaman",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "peerDependencies": {
+ "kysely": ">=0.28.16",
+ "kysely-bun-sqlite": ">=0.4.0"
+ },
+ "devDependencies": {
+ "kysely": "^0.28.16",
+ "kysely-bun-sqlite": "^0.4.0"
+ }
+}
diff --git a/packages/db/src/column/base-column.ts b/packages/db/src/column/base-column.ts
new file mode 100644
index 0000000..26288b4
--- /dev/null
+++ b/packages/db/src/column/base-column.ts
@@ -0,0 +1,8 @@
+import type { ColumnDataType } from 'kysely';
+import { ColumnDefinition } from './defintion-column';
+
+export class BaseColumnBuilder {
+ protected create(column: string, type: ColumnDataType) {
+ return new ColumnDefinition(column, type);
+ }
+}
diff --git a/packages/db/src/column/defintion-column.ts b/packages/db/src/column/defintion-column.ts
new file mode 100644
index 0000000..755c845
--- /dev/null
+++ b/packages/db/src/column/defintion-column.ts
@@ -0,0 +1,60 @@
+import type { ColumnDataType } from 'kysely';
+
+/**
+ * Disini ColumnDefintion itinya kayak config gitu jadi dari type kayak col.string() bakal lanjut ke defintion ini,
+ * Contoh: col.string().isPrimary()
+ */
+export class ColumnDefinition {
+ public isPrimary: boolean = false;
+ public isAutoIncrement: boolean = false;
+ public isNullable: boolean = false;
+ public isUnique: boolean = false;
+ public isUnsigned: boolean = false;
+
+ public isIndex: boolean = false;
+
+ public defaultValue?: any;
+
+ //? Properti bayangan untuk inferensi tipe data TypeScript
+ public _type!: T;
+
+ constructor(
+ public name: string,
+ public type: ColumnDataType | null,
+ ) {}
+
+ nullable() {
+ this.isNullable = true;
+ return this;
+ }
+
+ unique() {
+ this.isUnique = true;
+ return this;
+ }
+
+ primary() {
+ this.isPrimary = true;
+ this.isNullable = false;
+ return this;
+ }
+
+ autoIncrement() {
+ this.isAutoIncrement = true;
+ return this;
+ }
+
+ default(val: any | (() => any)) {
+ this.defaultValue = val;
+ return this;
+ }
+
+ unsigned() {
+ this.isUnsigned = true;
+ return this;
+ }
+
+ index() {
+ this.isIndex = true;
+ }
+}
diff --git a/packages/db/src/column/index.ts b/packages/db/src/column/index.ts
new file mode 100644
index 0000000..e1a185a
--- /dev/null
+++ b/packages/db/src/column/index.ts
@@ -0,0 +1,4 @@
+export * from './base-column';
+export * from './defintion-column';
+export * from './schema-column-builder';
+export * from './type-column';
diff --git a/packages/db/src/column/schema-column-builder.ts b/packages/db/src/column/schema-column-builder.ts
new file mode 100644
index 0000000..84fd27a
--- /dev/null
+++ b/packages/db/src/column/schema-column-builder.ts
@@ -0,0 +1,63 @@
+import type { ColumnDataType } from 'kysely';
+import { ColumnDefinition } from './defintion-column';
+import { BaseColumnBuilder } from './base-column';
+import type { BaseTypeColumn } from './type-column';
+
+const IS_SCHEMA_BUILDER = '__schema_builder__';
+
+export function isSchemaBuilder(def: ColumnDefinition): boolean {
+ return def.name === IS_SCHEMA_BUILDER;
+}
+
+export class SchemaColumnBuilder
+ extends BaseColumnBuilder
+ implements BaseTypeColumn
+{
+ protected override create(type: ColumnDataType): ColumnDefinition {
+ return new ColumnDefinition(IS_SCHEMA_BUILDER, type);
+ }
+
+ int() {
+ return this.create('integer');
+ }
+
+ bigInt() {
+ return this.create('bigint');
+ }
+
+ uuid() {
+ return this.create('varchar(36)');
+ }
+
+ string(len = 255) {
+ return this.create(`varchar(${len})`);
+ }
+
+ text() {
+ return this.create('text');
+ }
+
+ timestamp() {
+ return this.create('datetime');
+ }
+
+ boolean() {
+ return this.create('boolean');
+ }
+
+ float(float: 4 | 8 = 4) {
+ return this.create(`float${float}`);
+ }
+
+ decimal(precision = 10, scale = 2) {
+ return this.create(`decimal(${precision}, ${scale})`);
+ }
+
+ json(type: 'json' | 'jsonb' = 'json') {
+ return this.create(type);
+ }
+
+ date() {
+ return this.create('date');
+ }
+}
diff --git a/packages/db/src/column/type-column.ts b/packages/db/src/column/type-column.ts
new file mode 100644
index 0000000..18b0b89
--- /dev/null
+++ b/packages/db/src/column/type-column.ts
@@ -0,0 +1,37 @@
+import type { ColumnDefinition } from './defintion-column';
+
+type ColumnFn<
+ HasName extends boolean,
+ T,
+ Extra extends any[] = [],
+> = HasName extends true
+ ? (column: string, ...extra: Extra) => ColumnDefinition
+ : (...extra: Extra) => ColumnDefinition;
+
+export interface BaseTypeColumn {
+ int: ColumnFn;
+
+ bigInt: ColumnFn;
+
+ uuid: ColumnFn;
+
+ string: ColumnFn;
+
+ text: ColumnFn;
+
+ timestamp: ColumnFn;
+
+ boolean: ColumnFn;
+
+ float: ColumnFn;
+
+ decimal: ColumnFn;
+
+ json: (
+ ...args: HasName extends true
+ ? [column: string, type?: 'json' | 'jsonb']
+ : [type?: 'json' | 'jsonb']
+ ) => ColumnDefinition;
+
+ date: ColumnFn;
+}
diff --git a/packages/db/src/compose/index.ts b/packages/db/src/compose/index.ts
new file mode 100644
index 0000000..dfc6b1c
--- /dev/null
+++ b/packages/db/src/compose/index.ts
@@ -0,0 +1,3 @@
+export * from './schema';
+export * from './seeder';
+export * from './migration';
diff --git a/packages/db/src/compose/migration.ts b/packages/db/src/compose/migration.ts
new file mode 100644
index 0000000..eec73b8
--- /dev/null
+++ b/packages/db/src/compose/migration.ts
@@ -0,0 +1,48 @@
+import { MigrationModel } from '../database/MigrationModel';
+import { MigrationBuilder } from '../migration';
+import { Logger } from 'gaman/utils';
+
+export async function composeMigration(handlers: {
+ up: (m: MigrationBuilder) => Promise | void;
+ down: (m: MigrationBuilder) => Promise | void;
+}) {
+ await MigrationModel.sync();
+
+ return {
+ async runUp(filename: string) {
+ const found = await MigrationModel.findOne('name', filename);
+
+ if (found) {
+ Logger.error(
+ `Migration [${filename}] already executed. Use '--fresh' to drop and up again.`,
+ );
+ return;
+ }
+
+ Logger.info(`Migrating: ${filename}`);
+ await handlers.up(new MigrationBuilder());
+
+ if (!found) {
+ await MigrationModel.create({ name: filename });
+ }
+ Logger.info(`Migrated: ${filename}`);
+ },
+
+ async runDown(filename: string, force: boolean = false) {
+ const found = await MigrationModel.findOne('name', filename);
+
+ if (!found && !force) {
+ Logger.warn(
+ `Skip Rollback: [${filename}] has not been executed. Use '--force' to re-run.`,
+ );
+ return;
+ }
+
+ Logger.info(`Rolling back: ${filename}`);
+ await handlers.down(new MigrationBuilder());
+
+ if(found) await MigrationModel.delete(found.id);
+ Logger.info(`Rolled back: ${filename}`);
+ },
+ };
+}
diff --git a/packages/db/src/compose/schema.ts b/packages/db/src/compose/schema.ts
new file mode 100644
index 0000000..84081f3
--- /dev/null
+++ b/packages/db/src/compose/schema.ts
@@ -0,0 +1,21 @@
+import type { ColumnDefinition } from '../column/defintion-column';
+import { SchemaColumnBuilder } from '../column/schema-column-builder';
+import { RelationBuilder } from '../relation-builder';
+
+export type SchemaDefinition = Record;
+
+export function composeSchema(
+ name: string,
+ fields: (c: SchemaColumnBuilder) => T,
+ relations?: (r: RelationBuilder) => any,
+) {
+ return {
+ name,
+ fields: fields(new SchemaColumnBuilder()),
+ relations: relations?.(new RelationBuilder(name)),
+ //! Magic: Mengambil tipe data asli dari tiap ColumnBuilder
+ infer: {} as {
+ [K in keyof T]: T[K]['_type'];
+ },
+ };
+}
\ No newline at end of file
diff --git a/packages/db/src/compose/seeder.ts b/packages/db/src/compose/seeder.ts
new file mode 100644
index 0000000..23fdb24
--- /dev/null
+++ b/packages/db/src/compose/seeder.ts
@@ -0,0 +1,5 @@
+export type SeederHandler = () => Promise | void;
+
+export function composeSeeder( handler: SeederHandler) {
+ return handler;
+}
diff --git a/packages/db/src/create-db.ts b/packages/db/src/create-db.ts
new file mode 100644
index 0000000..e8d3196
--- /dev/null
+++ b/packages/db/src/create-db.ts
@@ -0,0 +1,66 @@
+import { Kysely, MysqlDialect, PostgresDialect } from 'kysely';
+import { BunSqliteDialect } from 'kysely-bun-sqlite';
+import { Database } from 'bun:sqlite';
+
+let db: Kysely | null = null;
+
+//! SINGLETON CONNECTION
+export function getDB() {
+ if (db) return db;
+
+ const connection = process.env.DB_CONNECTION || 'sqlite';
+
+ const config = {
+ host: process.env.DB_HOST || '127.0.0.1',
+ port: parseInt(process.env.DB_PORT || '0'),
+ database: process.env.DB_DATABASE || 'database.sqlite',
+ user: process.env.DB_USERNAME || 'root',
+ password: process.env.DB_PASSWORD || '',
+ };
+
+ function getDialect() {
+ switch (connection.toLowerCase()) {
+ case 'postgres':
+ case 'pg':
+ return new PostgresDialect({
+ pool: async () => {
+ const { Pool } = await import('pg');
+ return new Pool({
+ host: config.host,
+ port: config.port || 5432,
+ database: config.database,
+ user: config.user,
+ password: config.password,
+ });
+ },
+ });
+
+ case 'mysql':
+ case 'mysql2':
+ return new MysqlDialect({
+ pool: async () => {
+ const { createPool } = await import('mysql2');
+ return createPool({
+ host: config.host,
+ port: config.port || 3306,
+ database: config.database,
+ user: config.user,
+ password: config.password,
+ });
+ },
+ });
+
+ case 'sqlite':
+ default:
+ return new BunSqliteDialect({
+ database: new Database(config.database),
+ });
+ }
+ }
+
+ db = new Kysely({
+ dialect: getDialect(),
+ });
+
+ return db;
+}
\ No newline at end of file
diff --git a/packages/db/src/database/MigrationModel.ts b/packages/db/src/database/MigrationModel.ts
new file mode 100644
index 0000000..c84e0b9
--- /dev/null
+++ b/packages/db/src/database/MigrationModel.ts
@@ -0,0 +1,10 @@
+import { composeSchema } from '../compose';
+import { Model } from '../model';
+
+export const MigrationSchema = composeSchema('__gaman_migrations', (c) => ({
+ id: c.int().primary().autoIncrement(),
+ name: c.string().unique(),
+ createdAt: c.string().default(() => new Date().toISOString()),
+}));
+
+export const MigrationModel = new Model(MigrationSchema);
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
new file mode 100644
index 0000000..516e04b
--- /dev/null
+++ b/packages/db/src/index.ts
@@ -0,0 +1,2 @@
+export { Model } from './model';
+export * from './compose';
diff --git a/packages/db/src/migration/index.ts b/packages/db/src/migration/index.ts
new file mode 100644
index 0000000..45f9dab
--- /dev/null
+++ b/packages/db/src/migration/index.ts
@@ -0,0 +1,2 @@
+export * from './migration-builder';
+export * from './migration-column-builder';
diff --git a/packages/db/src/migration/migration-builder.ts b/packages/db/src/migration/migration-builder.ts
new file mode 100644
index 0000000..7ea7be9
--- /dev/null
+++ b/packages/db/src/migration/migration-builder.ts
@@ -0,0 +1,201 @@
+import { Kysely } from 'kysely';
+import { MigrationTableBuilder } from './migration-column-builder';
+import type { composeSchema } from '../compose';
+import { SchemaColumnBuilder } from '../column/schema-column-builder';
+import type { ColumnDefinition } from '../column/defintion-column';
+import { getDB } from '../create-db';
+
+export class MigrationBuilder {
+ private kysely = getDB();
+
+ /**
+ * CREATE TABLE
+ */
+ async createTable(tableName: string, cb: (t: MigrationTableBuilder) => void) {
+ const table = new MigrationTableBuilder();
+
+ cb(table);
+
+ let query = this.kysely.schema.createTable(tableName).ifNotExists();
+
+ const indexes: string[] = [];
+ for (const col of table.columns) {
+ if (!col.type) {
+ throw new Error(`Column "${col.name}" has no type defined`);
+ }
+
+ query = query.addColumn(col.name, col.type, (b) => {
+ let res = b;
+
+ if (col.isPrimary) res = res.primaryKey();
+ if (col.isAutoIncrement) res = res.autoIncrement();
+ if (col.isUnsigned) res = res.unsigned();
+ if (col.isUnique) res = res.unique();
+ if (!col.isNullable) res = res.notNull();
+ if (
+ col.defaultValue !== null &&
+ typeof col.defaultValue !== 'function'
+ ) {
+ res = res.defaultTo(col.defaultValue);
+ }
+
+ return res;
+ });
+
+ //? simpan index untuk dibuat nanti
+ if (col.isIndex) {
+ indexes.push(col.name);
+ }
+ }
+
+ const result = await query.execute();
+
+ // buat index setelah table dibuat
+ for (const colName of indexes) {
+ await this.kysely.schema
+ .createIndex(`${tableName}_${colName}_idx`)
+ .ifNotExists()
+ .on(tableName)
+ .column(colName)
+ .execute();
+ }
+
+ return result;
+ }
+
+ async createTableFromSchema(schema: ReturnType) {
+ let tableBuilder = this.kysely.schema
+ .createTable(schema.name)
+ .ifNotExists();
+
+ const indexes: string[] = [];
+
+ for (const [colName, colDefintion] of Object.entries(schema.fields)) {
+ if (!colDefintion.type) {
+ throw new Error(`Column "${colName}" has no type defined`);
+ }
+
+ tableBuilder = tableBuilder.addColumn(
+ colName,
+ colDefintion.type,
+ (cb) => {
+ let res = cb;
+
+ if (colDefintion.isPrimary) res = res.primaryKey();
+ if (colDefintion.isAutoIncrement) res = res.autoIncrement();
+ if (colDefintion.isUnsigned) res = res.unsigned();
+ if (colDefintion.isUnique) res = res.unique();
+ if (!colDefintion.isNullable) res = res.notNull();
+ if (
+ colDefintion.defaultValue !== undefined &&
+ colDefintion.defaultValue !== null &&
+ typeof colDefintion.defaultValue !== 'function'
+ ) {
+ res = res.defaultTo(colDefintion.defaultValue);
+ }
+
+ return res;
+ },
+ );
+
+ //? simpan index untuk dibuat nanti
+ if (colDefintion.isIndex) {
+ indexes.push(colName);
+ }
+ }
+
+ const result = await tableBuilder.execute();
+
+ // buat index setelah table dibuat
+ for (const colName of indexes) {
+ await this.kysely.schema
+ .createIndex(`${schema.name}_${colName}_idx`)
+ .ifNotExists()
+ .on(schema.name)
+ .column(colName)
+ .execute();
+ }
+
+ return result;
+ }
+
+ /**
+ * DROP TABLE
+ */
+ async dropTable(name: string) {
+ return this.kysely.schema.dropTable(name).ifExists().execute();
+ }
+
+ /**
+ * ADD COLUMN
+ */
+ async addColumn(
+ table: string,
+ name: string,
+ cb: (c: SchemaColumnBuilder) => ColumnDefinition,
+ ) {
+ const colDefintion = cb(new SchemaColumnBuilder());
+
+ const result = await this.kysely.schema
+ .alterTable(table)
+ .addColumn(name, colDefintion._type, (b) => {
+ let res = b;
+
+ if (colDefintion.isPrimary) res = res.primaryKey();
+ if (colDefintion.isAutoIncrement) res = res.autoIncrement();
+ if (colDefintion.isUnsigned) res = res.unsigned();
+ if (colDefintion.isUnique) res = res.unique();
+ if (!colDefintion.isNullable) res = res.notNull();
+ if (
+ colDefintion.defaultValue !== null &&
+ typeof colDefintion.defaultValue !== 'function'
+ ) {
+ res = res.defaultTo(colDefintion.defaultValue);
+ }
+ return res;
+ })
+ .execute();
+
+ if (colDefintion.isIndex) {
+ this.createIndex(table, colDefintion.name, colDefintion.isUnique);
+ }
+ }
+
+ /**
+ * DROP COLUMN
+ */
+ async dropColumn(table: string, name: string) {
+ return this.kysely.schema.alterTable(table).dropColumn(name).execute();
+ }
+
+ /**
+ * RENAME COLUMN
+ */
+ async renameColumn(table: string, from: string, to: string) {
+ return this.kysely.schema
+ .alterTable(table)
+ .renameColumn(from, to)
+ .execute();
+ }
+
+ /**
+ * CREATE INDEX
+ */
+ async createIndex(table: string, column: string, unique = false) {
+ let q = this.kysely.schema
+ .createIndex(`${table}_${column}_idx`)
+ .on(table)
+ .column(column);
+
+ if (unique) q = q.unique();
+
+ return q.execute();
+ }
+
+ /**
+ * DROP INDEX
+ */
+ async dropIndex(name: string) {
+ return this.kysely.schema.dropIndex(name).execute();
+ }
+}
diff --git a/packages/db/src/migration/migration-column-builder.ts b/packages/db/src/migration/migration-column-builder.ts
new file mode 100644
index 0000000..29a5f27
--- /dev/null
+++ b/packages/db/src/migration/migration-column-builder.ts
@@ -0,0 +1,63 @@
+import type { ColumnDataType } from 'kysely';
+import { ColumnDefinition } from '../column/defintion-column';
+import { BaseColumnBuilder } from '../column/base-column';
+import type { BaseTypeColumn } from '../column/type-column';
+
+export class MigrationTableBuilder
+ extends BaseColumnBuilder
+ implements BaseTypeColumn
+{
+ public columns: ColumnDefinition[] = [];
+
+ protected override create(
+ column: string,
+ type: ColumnDataType,
+ ): ColumnDefinition {
+ const col = new ColumnDefinition(column, type);
+ this.columns.push(col);
+ return col;
+ }
+ int(column: string) {
+ return this.create(column, 'integer');
+ }
+
+ bigInt(column: string) {
+ return this.create(column, 'bigint');
+ }
+
+ uuid(column: string) {
+ return this.create(column, 'varchar(36)');
+ }
+
+ string(column: string, len = 255) {
+ return this.create(column, `varchar(${len})`);
+ }
+
+ text(column: string) {
+ return this.create(column, 'text');
+ }
+
+ timestamp(column: string) {
+ return this.create(column, 'datetime');
+ }
+
+ boolean(column: string) {
+ return this.create(column, 'boolean');
+ }
+
+ float(column: string, float: 4 | 8 = 4) {
+ return this.create(column, `float${float}`);
+ }
+
+ decimal(column: string, precision = 10, scale = 2) {
+ return this.create(column, `decimal(${precision}, ${scale})`);
+ }
+
+ json(column: string, type: 'json' | 'jsonb' = 'json') {
+ return this.create(column, type);
+ }
+
+ date(column: string) {
+ return this.create(column, 'date');
+ }
+}
diff --git a/packages/db/src/model.ts b/packages/db/src/model.ts
new file mode 100644
index 0000000..75b3adc
--- /dev/null
+++ b/packages/db/src/model.ts
@@ -0,0 +1,265 @@
+import { Kysely } from 'kysely';
+import type { composeSchema } from './compose/schema';
+import type { ComparisonOperatorExpression } from 'kysely';
+import { getDB } from './create-db';
+import { Where } from './where';
+
+export class Model> {
+ /**
+ * @ID Instance database (singleton).
+ * @EN Singleton database instance.
+ */
+ public kysely: Kysely = getDB();
+
+ constructor(public schema: T) {}
+
+ //! init table
+ /**
+ * @ID Membuat tabel berdasarkan schema jika belum ada.
+ * @EN Creates table based on schema if it does not exist.
+ */
+ async sync() {
+ let tableBuilder = this.kysely.schema
+ .createTable(this.schema.name)
+ .ifNotExists();
+
+ const indexes: string[] = [];
+
+ for (const [colName, colDefintion] of Object.entries(this.schema.fields)) {
+ if (!colDefintion.type) {
+ throw new Error(`Column "${colName}" has no type defined`);
+ }
+
+ tableBuilder = tableBuilder
+ .addColumn(colName, colDefintion.type, (cb) => {
+ let res = cb;
+
+ if (colDefintion.isPrimary) res = res.primaryKey();
+ if (colDefintion.isAutoIncrement) res = res.autoIncrement();
+ if (colDefintion.isUnsigned) res = res.unsigned();
+ if (colDefintion.isUnique) res = res.unique();
+ if (!colDefintion.isNullable) res = res.notNull();
+ if (
+ colDefintion.defaultValue !== undefined &&
+ colDefintion.defaultValue !== null &&
+ typeof colDefintion.defaultValue !== 'function'
+ ) {
+ res = res.defaultTo(colDefintion.defaultValue);
+ }
+
+ return res;
+ })
+ .ifNotExists();
+
+ //? simpan index untuk dibuat nanti
+ if (colDefintion.isIndex) {
+ indexes.push(colName);
+ }
+ }
+
+ const result = await tableBuilder.execute();
+
+ // buat index setelah table dibuat
+ for (const colName of indexes) {
+ await this.kysely.schema
+ .createIndex(`${this.schema.name}_${colName}_idx`)
+ .ifNotExists()
+ .on(this.schema.name)
+ .column(colName)
+ .execute();
+ }
+
+ return result;
+ }
+
+ /**
+ * @ID Base query builder untuk tabel ini.
+ * @EN Base query builder for this table.
+ */
+ query() {
+ return this.kysely.selectFrom(this.schema.name);
+ }
+
+ /**
+ * @ID Mengambil semua data dari tabel.
+ * @EN Fetches all records from the table.
+ */
+ async all(): Promise> {
+ return await this.query().selectAll().execute();
+ }
+
+ /**
+ * @ID Mencari satu rekaman berdasarkan nama kolom dan nilai tertentu.
+ * @EN Finds multiple record by a specific column and value.
+ */
+ async find(column: keyof T['infer'], value: any): Promise> {
+ return await this.query()
+ .selectAll()
+ .where(column as any, '=', value)
+ .execute();
+ }
+
+ /**
+ * @ID Mencari satu data berdasarkan kolom.
+ * @EN Finds a single record by column.
+ */
+ async findOne(
+ column: keyof T['infer'],
+ value: any,
+ ): Promise {
+ return await this.query()
+ .selectAll()
+ .where(column as any, '=', value)
+ .executeTakeFirst();
+ }
+
+ /**
+ * @ID Query filter berbasis kondisi (return query builder).
+ * @EN Conditional query filter (returns query builder).
+ */
+ where(
+ column: keyof T['infer'],
+ op: ComparisonOperatorExpression,
+ value: any,
+ ) {
+ return new Where(this.kysely, this.schema.name).where(
+ column,
+ op,
+ value,
+ );
+ }
+ /**
+ * @ID Membuat data baru ke tabel.
+ * @EN Inserts new record into table.
+ */
+ async create(data: Partial) {
+ const finalData: any = { ...data };
+
+ for (const [key, col] of Object.entries(this.schema.fields)) {
+ if (finalData[key] === undefined && col.defaultValue !== null) {
+ if (typeof col.defaultValue === 'function') {
+ finalData[key] = col.defaultValue();
+ } else {
+ finalData[key] = col.defaultValue;
+ }
+ }
+ }
+
+ return await this.kysely
+ .insertInto(this.schema.name)
+ .values(finalData)
+ .executeTakeFirstOrThrow();
+ }
+
+ /**
+ * @ID Membuat banyak data sekaligus ke tabel.
+ * @EN Inserts multiple records into the table.
+ */
+ async createMany(data: Array>) {
+ const finalData = data.map((item) => {
+ const row: any = { ...item };
+
+ for (const [key, col] of Object.entries(this.schema.fields)) {
+ if (row[key] === undefined && col.defaultValue !== null) {
+ if (typeof col.defaultValue === 'function') {
+ row[key] = col.defaultValue();
+ } else {
+ row[key] = col.defaultValue;
+ }
+ }
+ }
+
+ return row;
+ });
+
+ return await this.kysely
+ .insertInto(this.schema.name)
+ .values(finalData)
+ .execute();
+ }
+
+ /**
+ * @ID Memperbarui data berdasarkan ID unik.
+ * @EN Updates a record based on its unique ID.
+ */
+ async update(id: number | string, data: Partial) {
+ return await this.kysely
+ .updateTable(this.schema.name)
+ .set(data as any)
+ .where('id' as any, '=', id as any)
+ .execute();
+ }
+
+ /**
+ * @ID Menghapus rekaman berdasarkan ID unik.
+ * @EN Deletes a record based on its unique ID.
+ */
+ async delete(id: number | string) {
+ return await this.kysely
+ .deleteFrom(this.schema.name)
+ .where('id' as any, '=', id as any)
+ .execute();
+ }
+
+ /**
+ * @ID Menghitung total jumlah rekaman dalam tabel.
+ * @EN Counts the total number of records in the table.
+ */
+ async count() {
+ const result = await this.query()
+ .select((eb: any) => eb.fn.countAll().as('total'))
+ .executeTakeFirst();
+
+ return Number((result as any)?.total || 0);
+ }
+
+ /**
+ * @ID Mengambil rekaman terbaru berdasarkan urutan kolom tertentu.
+ * @EN Retrieves the latest record ordered by a specific column.
+ */
+ async latest(column: keyof T['infer'] = 'created_at' as any) {
+ return await this.query()
+ .selectAll()
+ .orderBy(column as any, 'desc')
+ .executeTakeFirst();
+ }
+
+ /**
+ * @ID Mengambil data dengan sistem paginasi.
+ * @EN Retrieves data using a pagination system.
+ */
+ async paginate(page: number = 1, limit: number = 15) {
+ const offset = (page - 1) * limit;
+
+ const [data, total] = await Promise.all([
+ this.query().selectAll().limit(limit).offset(offset).execute(),
+ this.count(),
+ ]);
+
+ return {
+ data,
+ meta: {
+ total,
+ page,
+ limit,
+ lastPage: Math.ceil(total / limit),
+ },
+ };
+ }
+
+ /**
+ * @ID Mengembalikan instance Kysely utama untuk akses database tingkat rendah.
+ * @EN Returns the raw Kysely instance for low-level database access.
+ */
+ getRaw() {
+ return this.kysely;
+ }
+
+ /**
+ * @ID Menghapus semua data di dalam tabel (bersihkan tabel).
+ * @EN Deletes all data within the table (clears the table).
+ */
+ async truncate() {
+ return await this.kysely.deleteFrom(this.schema.name).execute();
+ }
+}
diff --git a/packages/db/src/relation-builder.ts b/packages/db/src/relation-builder.ts
new file mode 100644
index 0000000..2b6e71d
--- /dev/null
+++ b/packages/db/src/relation-builder.ts
@@ -0,0 +1,29 @@
+type RelationType = 'hasOne' | 'hasMany' | 'belongsTo';
+
+export class RelationBuilder {
+ constructor(private currentTable: string) {}
+
+ hasMany(target: any, config: { foreignKey: string }) {
+ return {
+ type: 'hasMany' as RelationType,
+ target,
+ foreignKey: config.foreignKey,
+ };
+ }
+
+ belongsTo(target: any, config: { foreignKey: string }) {
+ return {
+ type: 'belongsTo' as RelationType,
+ target,
+ foreignKey: config.foreignKey,
+ };
+ }
+
+ hasOne(target: any, config: { foreignKey: string }) {
+ return {
+ type: 'hasOne' as RelationType,
+ target,
+ foreignKey: config.foreignKey,
+ };
+ }
+}
\ No newline at end of file
diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts
new file mode 100644
index 0000000..00342db
--- /dev/null
+++ b/packages/db/src/types.ts
@@ -0,0 +1,139 @@
+declare const SIMPLE_COLUMN_DATA_TYPES: readonly [
+ 'varchar',
+ 'char',
+ 'text',
+ 'integer',
+ 'int2',
+ 'int4',
+ 'int8',
+ 'smallint',
+ 'bigint',
+ 'boolean',
+ 'real',
+ 'double precision',
+ 'float4',
+ 'float8',
+ 'decimal',
+ 'numeric',
+ 'binary',
+ 'bytea',
+ 'date',
+ 'datetime',
+ 'time',
+ 'timetz',
+ 'timestamp',
+ 'timestamptz',
+ 'serial',
+ 'bigserial',
+ 'uuid',
+ 'json',
+ 'jsonb',
+ 'blob',
+ 'varbinary',
+ 'int4range',
+ 'int4multirange',
+ 'int8range',
+ 'int8multirange',
+ 'numrange',
+ 'nummultirange',
+ 'tsrange',
+ 'tsmultirange',
+ 'tstzrange',
+ 'tstzmultirange',
+ 'daterange',
+ 'datemultirange',
+];
+type SimpleColumnDataType = (typeof SIMPLE_COLUMN_DATA_TYPES)[number];
+export type ColumnDataType =
+ | SimpleColumnDataType
+ | `varchar(${number})`
+ | `char(${number})`
+ | `decimal(${number}, ${number})`
+ | `numeric(${number}, ${number})`
+ | `binary(${number})`
+ | `datetime(${number})`
+ | `time(${number})`
+ | `timetz(${number})`
+ | `timestamp(${number})`
+ | `timestamptz(${number})`
+ | `varbinary(${number})`;
+
+const sql = {
+ // String types
+ text: (): 'text' => 'text',
+ varchar: (n?: number): 'varchar' | `varchar(${number})` =>
+ n !== undefined ? `varchar(${n})` : 'varchar',
+ char: (n?: number): 'char' | `char(${number})` =>
+ n !== undefined ? `char(${n})` : 'char',
+
+ // Integer types
+ integer: (): 'integer' => 'integer',
+ int2: (): 'int2' => 'int2',
+ int4: (): 'int4' => 'int4',
+ int8: (): 'int8' => 'int8',
+ smallint: (): 'smallint' => 'smallint',
+ bigint: (): 'bigint' => 'bigint',
+ serial: (): 'serial' => 'serial',
+ bigserial: (): 'bigserial' => 'bigserial',
+
+ // Boolean
+ boolean: (): 'boolean' => 'boolean',
+
+ // Float types
+ real: (): 'real' => 'real',
+ doublePrecision: (): 'double precision' => 'double precision',
+ float4: (): 'float4' => 'float4',
+ float8: (): 'float8' => 'float8',
+
+ // Decimal / Numeric
+ decimal: (precision?: number, scale?: number): 'decimal' | `decimal(${number}, ${number})` =>
+ precision !== undefined && scale !== undefined
+ ? `decimal(${precision}, ${scale})`
+ : 'decimal',
+ numeric: (precision?: number, scale?: number): 'numeric' | `numeric(${number}, ${number})` =>
+ precision !== undefined && scale !== undefined
+ ? `numeric(${precision}, ${scale})`
+ : 'numeric',
+
+ // Binary types
+ binary: (n?: number): 'binary' | `binary(${number})` =>
+ n !== undefined ? `binary(${n})` : 'binary',
+ bytea: (): 'bytea' => 'bytea',
+ blob: (): 'blob' => 'blob',
+ varbinary: (n?: number): 'varbinary' | `varbinary(${number})` =>
+ n !== undefined ? `varbinary(${n})` : 'varbinary',
+
+ // Date/Time types
+ date: (): 'date' => 'date',
+ datetime: (n?: number): 'datetime' | `datetime(${number})` =>
+ n !== undefined ? `datetime(${n})` : 'datetime',
+ time: (n?: number): 'time' | `time(${number})` =>
+ n !== undefined ? `time(${n})` : 'time',
+ timetz: (n?: number): 'timetz' | `timetz(${number})` =>
+ n !== undefined ? `timetz(${n})` : 'timetz',
+ timestamp: (n?: number): 'timestamp' | `timestamp(${number})` =>
+ n !== undefined ? `timestamp(${n})` : 'timestamp',
+ timestamptz: (n?: number): 'timestamptz' | `timestamptz(${number})` =>
+ n !== undefined ? `timestamptz(${n})` : 'timestamptz',
+
+ // UUID
+ uuid: (): 'uuid' => 'uuid',
+
+ // JSON types
+ json: (): 'json' => 'json',
+ jsonb: (): 'jsonb' => 'jsonb',
+
+ // Range types
+ int4range: (): 'int4range' => 'int4range',
+ int4multirange: (): 'int4multirange' => 'int4multirange',
+ int8range: (): 'int8range' => 'int8range',
+ int8multirange: (): 'int8multirange' => 'int8multirange',
+ numrange: (): 'numrange' => 'numrange',
+ nummultirange: (): 'nummultirange' => 'nummultirange',
+ tsrange: (): 'tsrange' => 'tsrange',
+ tsmultirange: (): 'tsmultirange' => 'tsmultirange',
+ tstzrange: (): 'tstzrange' => 'tstzrange',
+ tstzmultirange: (): 'tstzmultirange' => 'tstzmultirange',
+ daterange: (): 'daterange' => 'daterange',
+ datemultirange: (): 'datemultirange' => 'datemultirange',
+} satisfies Record ColumnDataType>;
\ No newline at end of file
diff --git a/packages/db/src/where.ts b/packages/db/src/where.ts
new file mode 100644
index 0000000..3df4166
--- /dev/null
+++ b/packages/db/src/where.ts
@@ -0,0 +1,144 @@
+import type { ComparisonOperatorExpression } from 'kysely';
+import type { Kysely, SelectQueryBuilder } from 'kysely';
+
+export type WhereCondition = [keyof T, ComparisonOperatorExpression, any];
+
+export class Where> {
+ private conditions: WhereCondition[] = [];
+ private qb: SelectQueryBuilder;
+
+ constructor(
+ private kysely: Kysely,
+ private table: string,
+ ) {
+ this.qb = this.kysely.selectFrom(this.table);
+ }
+
+ private applyWhere(query: Q): Q {
+ let q = query;
+
+ for (const [col, op, val] of this.conditions) {
+ q = q.where(col as string, op, val);
+ }
+
+ return q;
+ }
+
+ /**
+ * @ID Tambah kondisi where.
+ * @EN Adds where condition.
+ */
+ where(column: keyof T, op: ComparisonOperatorExpression, value: any) {
+ this.conditions.push([column, op, value]);
+ this.qb = this.qb.where(column as string, op, value);
+ return this;
+ }
+
+ /**
+ * @ID Memperbarui data berdasarkan model.
+ * @EN Updates a record based on its model.
+ */
+ async update(data: Partial) {
+ let q = this.kysely.updateTable(this.table).set(data as any);
+
+ q = this.applyWhere(q);
+
+ return await q.execute();
+ }
+
+ /**
+ * @ID Menghapus rekaman berdasarkan model.
+ * @EN Deletes a record based on its model.
+ */
+ async delete() {
+ let q = this.kysely.deleteFrom(this.table);
+
+ q = this.applyWhere(q);
+
+ return await q.execute();
+ }
+
+ /**
+ * @ID Sorting data (type-safe column).
+ * @EN Orders query result (type-safe column).
+ */
+ orderBy(column: keyof T, direction: 'asc' | 'desc' = 'asc') {
+ this.qb = this.qb.orderBy(column as string, direction);
+ return this;
+ }
+
+ /**
+ * @ID Batasi jumlah data.
+ * @EN Limits query result.
+ */
+ limit(n: number) {
+ this.qb = this.qb.limit(n);
+ return this;
+ }
+
+ /**
+ * @ID Offset data.
+ * @EN Sets query offset.
+ */
+ offset(n: number) {
+ this.qb = this.qb.offset(n);
+ return this;
+ }
+
+ /**
+ * @ID Mengambil semua hasil query.
+ * @EN Executes query and returns all results.
+ */
+ async get(): Promise {
+ return await this.qb.selectAll().execute();
+ }
+
+ /**
+ * @ID Mengambil satu data pertama.
+ * @EN Executes query and returns first result.
+ */
+ async first(): Promise {
+ return await this.qb.selectAll().executeTakeFirst();
+ }
+
+ /**
+ * @ID Menghitung total jumlah rekaman dalam query.
+ * @EN Counts the total number of records in the query.
+ */
+ async count() {
+ const result = await this.qb
+ .clearSelect()
+ .select((eb: any) => eb.fn.countAll().as('total'))
+ .executeTakeFirst();
+
+ return Number((result as any)?.total || 0);
+ }
+
+ /**
+ * @ID Pagination query.
+ * @EN Paginate query results.
+ */
+ async paginate(page: number = 1, limit: number = 15) {
+ const offset = (page - 1) * limit;
+
+ const [data, totalResult] = await Promise.all([
+ this.qb.selectAll().limit(limit).offset(offset).execute(),
+ this.qb
+ .clearSelect()
+ .select((eb: any) => eb.fn.countAll().as('total'))
+ .executeTakeFirst(),
+ ]);
+
+ const total = Number((totalResult as any)?.total || 0);
+
+ return {
+ data,
+ meta: {
+ total,
+ page,
+ limit,
+ lastPage: Math.ceil(total / limit),
+ },
+ };
+ }
+}
diff --git a/packages/db/tsconfig.dts.json b/packages/db/tsconfig.dts.json
new file mode 100644
index 0000000..e5c05ba
--- /dev/null
+++ b/packages/db/tsconfig.dts.json
@@ -0,0 +1,60 @@
+{
+ "compilerOptions": {
+ "preserveSymlinks": true,
+ "target": "ES2021",
+ "lib": [
+ "ESNext"
+ ],
+ "module": "ES2022",
+ "rootDir": "./src",
+ "outDir": "./dist",
+
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "baseUrl": ".",
+ "paths": {
+ "gaman": [
+ "../../dist/index.d.ts"
+ ],
+ "gaman/types": [
+ "../../dist/types.d.ts"
+ ],
+ "gaman/responder": [
+ "../../dist/responder.d.ts"
+ ],
+ "gaman/compose": [
+ "../../dist/compose/index.d.ts"
+ ],
+ "gaman/utils": [
+ "../../dist/utils/index.d.ts"
+ ],
+ "gaman/formdata": [
+ "../../dist/context/formdata/index.d.ts"
+ ],
+ "gaman/header": [
+ "../../dist/context/header/index.d.ts"
+ ],
+ "gaman/enums": [
+ "../../dist/enums/index.d.ts"
+ ]
+ }
+ },
+ "include": [
+ "src/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "test",
+ "example",
+ "dist",
+ "build.ts",
+ "old",
+ "src-test",
+ "**/*.test.*",
+ "benchmark",
+ "**/*.benchmark.*"
+ ]
+}
\ No newline at end of file
diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json
new file mode 100644
index 0000000..14dba04
--- /dev/null
+++ b/packages/db/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./dist"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/kame/README.md b/packages/kame/README.md
index d7dfca1..2c69b8c 100644
--- a/packages/kame/README.md
+++ b/packages/kame/README.md
@@ -1,21 +1 @@
-# @gaman/static
-**Secure, Lightweight & High-Performance Static File Server for GamanJS**. Built for Bun, optimized for speed with non-blocking I/O, built-in compression, and ETag caching.
-
-## Installation
-```bash
-bun add @gaman/static
-```
-
-## Quick Used
-By default, this middleware will serve files from the `public/` folder in the root of your project.
-```ts
-import { defineBootstrap } from "gaman";
-import { StaticServe } from "@gaman/static";
-
-defineBootstrap((app) => {
- // Mount the static server
- app.mount(StaticServe());
-
- app.mountServer(...)
-});
-```
\ No newline at end of file
+# @gaman/kame
\ No newline at end of file
diff --git a/packages/kame/src/commands/database/migrate.ts b/packages/kame/src/commands/database/migrate.ts
new file mode 100644
index 0000000..d7c3f26
--- /dev/null
+++ b/packages/kame/src/commands/database/migrate.ts
@@ -0,0 +1,73 @@
+import { registerCommand } from '../registry';
+import { composeMigration } from '@gaman/db';
+import { Logger } from 'gaman/utils';
+import { join } from 'path';
+import { readdirSync } from 'fs';
+
+registerCommand({
+ name: 'db:migrate',
+ description: 'Execute database migrations',
+ usage: 'db:migrate [filename] [--fresh] [--force]',
+ aliases: ['migrate'],
+ handler: async (args, flags, cfg) => {
+ const targetFile = args[0];
+ const srcDir = cfg.srcDir || 'src';
+ const migrationDir = join(process.cwd(), srcDir, 'database', 'migrations');
+
+ try {
+ let filesToProcess: string[] = [];
+
+ if (targetFile) {
+ filesToProcess = [targetFile];
+ } else {
+ // Pastikan readdirSync tidak error jika folder belum ada
+ const allFiles = readdirSync(migrationDir, { withFileTypes: true });
+ filesToProcess = allFiles
+ .filter(
+ (dirent) =>
+ dirent.isFile() &&
+ (dirent.name.endsWith('.ts') || dirent.name.endsWith('.js')),
+ )
+ .map((dirent) => dirent.name)
+ .sort((a, b) => a.localeCompare(b));
+ }
+
+ if (filesToProcess.length === 0) {
+ Logger.info('No migrations found to execute.');
+ return;
+ }
+
+ for (const filename of filesToProcess) {
+ // 1. Validasi Path agar tidak undefined
+ const filePath = join(migrationDir, filename);
+
+ // 2. Import Module
+ const migrationModule = await import(filePath);
+
+ // 3. Ambil default export atau module itu sendiri
+ // Gunakan await lagi jika composeMigration adalah async function
+ const migration = await (migrationModule.default || migrationModule);
+
+ // 4. Cek apakah method runUp ada sebelum dipanggil
+ if (!migration || typeof migration.runUp !== 'function') {
+ Logger.error(
+ `Invalid migration format in: ${filename}. Make sure to use export default composeMigration(...)`,
+ );
+ continue;
+ }
+
+ if ('fresh' in flags) {
+ await migration.runDown(filename, true);
+ }
+
+ await migration.runUp(filename, 'force' in flags);
+ }
+
+ Logger.info('Migration process completed.');
+ } catch (error: any) {
+ // Tampilkan stack trace agar tahu baris mana yang error
+ Logger.error(`Migration failed: ${error.message}`);
+ if (error.stack) console.error(error.stack);
+ }
+ },
+});
diff --git a/packages/kame/src/commands/database/seed.ts b/packages/kame/src/commands/database/seed.ts
new file mode 100644
index 0000000..e95672a
--- /dev/null
+++ b/packages/kame/src/commands/database/seed.ts
@@ -0,0 +1,25 @@
+import { registerCommand } from '../registry';
+import { composeSeeder } from '@gaman/db';
+import { Logger } from 'gaman/utils';
+import { join } from 'path';
+registerCommand({
+ name: 'db:seed',
+ description: 'Execute migration file',
+ usage: 'db:seed ',
+ aliases: [],
+ handler: async (args, flags, cfg) => {
+ const filename = args[0];
+ if (filename == undefined) {
+ Logger.error(`Usage: db:seed `);
+ return;
+ }
+ Logger.info(`seeder ${filename} is already running...`);
+ let seeder: ReturnType = await import(
+ join(process.cwd(), cfg.srcDir || 'src', 'database', 'seeders', filename)
+ );
+ if ((seeder as any).default) seeder = (seeder as any).default;
+
+ await seeder();
+ Logger.info(`seeder ${filename} process has been completed`);
+ },
+});
diff --git a/packages/kame/src/commands/gen-controller.ts b/packages/kame/src/commands/gen-controller.ts
index 4c546f0..cc646c0 100644
--- a/packages/kame/src/commands/gen-controller.ts
+++ b/packages/kame/src/commands/gen-controller.ts
@@ -3,8 +3,13 @@ import { join, relative } from 'node:path';
import { registerCommand } from './registry';
import { controllerTemplate } from '../templates/module';
import { capitalize } from '../utils';
+import type { KameConfig } from '../repl';
-const handler = async (args: string[]): Promise => {
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
const [name, module = 'app'] = args;
if (!name || !module) {
Logger.error("Usage: gen:controller ");
@@ -14,7 +19,13 @@ const handler = async (args: string[]): Promise => {
const nameCapitalized = capitalize(name);
const cwd = process.cwd();
// Support nested paths like "v2/user"
- const controllerDir = join(cwd, 'src', 'modules', module, 'controllers');
+ const controllerDir = join(
+ cwd,
+ cfg.srcDir || 'src',
+ 'modules',
+ module,
+ 'controllers',
+ );
const filePath = join(controllerDir, `${nameCapitalized}Controller.ts`);
await Bun.$`mkdir -p ${controllerDir}`.quiet();
diff --git a/packages/kame/src/commands/gen-exception.ts b/packages/kame/src/commands/gen-exception.ts
index e2bf59a..858ecc8 100644
--- a/packages/kame/src/commands/gen-exception.ts
+++ b/packages/kame/src/commands/gen-exception.ts
@@ -3,8 +3,13 @@ import { join, relative } from 'node:path';
import { registerCommand } from './registry';
import { exceptionTemplate } from '../templates/module';
import { capitalize } from '../utils';
+import type { KameConfig } from '../repl';
-const handler = async (args: string[]): Promise => {
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
const [name, module = 'app'] = args;
if (!name || !module) {
Logger.error("Usage: gen:exception ");
@@ -15,7 +20,13 @@ const handler = async (args: string[]): Promise => {
const cwd = process.cwd();
// Support nested paths like "v2/user"
- const exceptionDir = join(cwd, 'src', 'modules', module, 'exceptions');
+ const exceptionDir = join(
+ cwd,
+ cfg.srcDir || 'src',
+ 'modules',
+ module,
+ 'exceptions',
+ );
const filePath = join(exceptionDir, `${nameCapitalized}Exception.ts`);
await Bun.$`mkdir -p ${exceptionDir}`.quiet();
diff --git a/packages/kame/src/commands/gen-middleware.ts b/packages/kame/src/commands/gen-middleware.ts
index f2e27ca..07314f2 100644
--- a/packages/kame/src/commands/gen-middleware.ts
+++ b/packages/kame/src/commands/gen-middleware.ts
@@ -3,8 +3,13 @@ import { join, relative } from 'node:path';
import { registerCommand } from './registry';
import { middlewareTemplate } from '../templates/module';
import { capitalize } from '../utils';
+import type { KameConfig } from '../repl';
-const handler = async (args: string[]): Promise => {
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
const [name, module = 'app'] = args;
if (!name || !module) {
Logger.error("Usage: gen:middleware ");
@@ -14,7 +19,7 @@ const handler = async (args: string[]): Promise => {
const nameCapitalized = capitalize(name);
const cwd = process.cwd();
// Support nested paths like "v2/user"
- const middlewareDir = join(cwd, 'src', 'modules', module, 'middlewares');
+ const middlewareDir = join(cwd, cfg.srcDir || 'src', 'modules', module, 'middlewares');
const filePath = join(middlewareDir, `${nameCapitalized}Middleware.ts`);
await Bun.$`mkdir -p ${middlewareDir}`.quiet();
diff --git a/packages/kame/src/commands/gen-migration.ts b/packages/kame/src/commands/gen-migration.ts
new file mode 100644
index 0000000..7c75194
--- /dev/null
+++ b/packages/kame/src/commands/gen-migration.ts
@@ -0,0 +1,62 @@
+import { Logger } from 'gaman/utils';
+import { join, relative } from 'node:path';
+import { registerCommand } from './registry';
+import { migrationTemplate } from '../templates/database'; // Import template kamu
+import type { KameConfig } from '../repl';
+
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
+ const [name] = args;
+
+ if (!name) {
+ Logger.error("Usage: gen:migration ");
+ return;
+ }
+
+ const cwd = process.cwd();
+
+ // 1. Generate Timestamp (YYYY_MM_DD_HHMMSS)
+ const now = new Date();
+ const timestamp = now.getFullYear().toString() +
+ (now.getMonth() + 1).toString().padStart(2, '0') +
+ now.getDate().toString().padStart(2, '0') + '_' +
+ now.getHours().toString().padStart(2, '0') +
+ now.getMinutes().toString().padStart(2, '0') +
+ now.getSeconds().toString().padStart(2, '0');
+
+ // 2. Tentukan direktori migrations
+ const migrationDir = join(
+ cwd,
+ cfg.srcDir || 'src',
+ 'database',
+ 'migrations',
+ );
+
+ // 3. Nama file: timestamp_nama_migration.ts
+ const fileName = `${timestamp}_${name.toLowerCase().replace(/\s+/g, '_')}.ts`;
+ const filePath = join(migrationDir, fileName);
+
+ try {
+ // Buat folder jika belum ada
+ await Bun.$`mkdir -p ${migrationDir}`.quiet();
+
+ // Tulis file menggunakan template yang kamu buat
+ await Bun.write(filePath, migrationTemplate() + '\n');
+
+ Logger.info(`created ${relative(cwd, filePath)}`);
+ Logger.info(`Migration "${fileName}" generated successfully.`);
+ } catch (error: any) {
+ Logger.error(`Failed to generate migration: ${error.message}`);
+ }
+};
+
+registerCommand({
+ name: 'gen:migration',
+ description: 'Generate a new database migration file with timestamp',
+ usage: "gen:migration ",
+ aliases: ['gen:mi', 'make:migration'],
+ handler,
+});
\ No newline at end of file
diff --git a/packages/kame/src/commands/gen-module.ts b/packages/kame/src/commands/gen-module.ts
index 192300b..46a8aaf 100644
--- a/packages/kame/src/commands/gen-module.ts
+++ b/packages/kame/src/commands/gen-module.ts
@@ -7,8 +7,13 @@ import {
serviceTemplate,
} from '../templates/module';
import { capitalize } from '../utils';
+import type { KameConfig } from '../repl';
-const handler = async (args: string[]): Promise => {
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
const modulePath = args[0];
if (!modulePath) {
Logger.error('Usage: gen:module ');
@@ -19,7 +24,7 @@ const handler = async (args: string[]): Promise => {
const name = basename(modulePath);
const nameCapitalized = capitalize(name);
const cwd = process.cwd();
- const moduleDir = join(cwd, 'src', 'modules', modulePath);
+ const moduleDir = join(cwd, cfg.srcDir || 'src', 'modules', modulePath);
const files: { filePath: string; content: string }[] = [
{
diff --git a/packages/kame/src/commands/gen-router.ts b/packages/kame/src/commands/gen-router.ts
index 9178782..f674fd8 100644
--- a/packages/kame/src/commands/gen-router.ts
+++ b/packages/kame/src/commands/gen-router.ts
@@ -3,8 +3,13 @@ import { join, relative } from 'node:path';
import { registerCommand } from './registry';
import { routerBlankTemplate } from '../templates/module';
import { capitalize } from '../utils';
+import type { KameConfig } from '../repl';
-const handler = async (args: string[]): Promise => {
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
const [name, module = 'app'] = args;
if (!name || !module) {
Logger.error("Usage: gen:router ");
@@ -14,7 +19,7 @@ const handler = async (args: string[]): Promise => {
const nameCapitalized = capitalize(name);
const cwd = process.cwd();
// Support nested paths like "v2/user"
- const routerDir = join(cwd, 'src', 'modules', module);
+ const routerDir = join(cwd, cfg.srcDir || 'src', 'modules', module);
const filePath = join(routerDir, `${nameCapitalized}Router.ts`);
await Bun.$`mkdir -p ${routerDir}`.quiet();
diff --git a/packages/kame/src/commands/gen-seeder.ts b/packages/kame/src/commands/gen-seeder.ts
new file mode 100644
index 0000000..c0c8cd9
--- /dev/null
+++ b/packages/kame/src/commands/gen-seeder.ts
@@ -0,0 +1,46 @@
+import { Logger } from 'gaman/utils';
+import { join, relative } from 'node:path';
+import { registerCommand } from './registry';
+import type { KameConfig } from '../repl';
+import { seederTemplate } from '../templates/database';
+
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
+ const [name] = args;
+
+ if (!name) {
+ Logger.error('Usage: gen:seeder ');
+ return;
+ }
+
+ const cwd = process.cwd();
+
+ const seederDir = join(cwd, cfg.srcDir || 'src', 'database', 'seeders');
+
+ const fileName = name.includes('.ts') ? name : `${name}.ts`;
+ const filePath = join(seederDir, fileName);
+
+ try {
+ // Buat folder jika belum ada
+ await Bun.$`mkdir -p ${seederDir}`.quiet();
+
+ // Tulis file menggunakan template yang kamu buat
+ await Bun.write(filePath, seederTemplate() + '\n');
+
+ Logger.info(`created ${relative(cwd, filePath)}`);
+ Logger.info(`Seeder "${fileName}" generated successfully.`);
+ } catch (error: any) {
+ Logger.error(`Failed to generate seeder: ${error.message}`);
+ }
+};
+
+registerCommand({
+ name: 'gen:seeder',
+ description: 'Generate a new database seeder file with timestamp',
+ usage: 'gen:seeder ',
+ aliases: ['gen:seed', 'make:seeder'],
+ handler,
+});
diff --git a/packages/kame/src/commands/gen-service.ts b/packages/kame/src/commands/gen-service.ts
index 8f8ecd4..181d87b 100644
--- a/packages/kame/src/commands/gen-service.ts
+++ b/packages/kame/src/commands/gen-service.ts
@@ -3,6 +3,7 @@ import { join, relative, basename } from 'node:path';
import { registerCommand } from './registry';
import { standaloneServiceTemplate } from '../templates/service';
import { capitalize, toCamelCase } from '../utils';
+import type { KameConfig } from '../repl';
/**
* Inject the new service into the module Router file.
@@ -62,7 +63,11 @@ const patchRouter = async (
await Bun.write(routerPath, source);
};
-const handler = async (args: string[]): Promise => {
+const handler = async (
+ args: string[],
+ flags: any,
+ cfg: KameConfig,
+): Promise => {
const [name, module = 'app'] = args;
if (!name || !module) {
Logger.error("Usage: gen:service ");
@@ -74,7 +79,13 @@ const handler = async (args: string[]): Promise => {
const moduleSegment = basename(module);
const moduleCapitalized = capitalize(moduleSegment);
const cwd = process.cwd();
- const serviceDir = join(cwd, 'src', 'modules', module, 'services');
+ const serviceDir = join(
+ cwd,
+ cfg.srcDir || 'src',
+ 'modules',
+ module,
+ 'services',
+ );
const filePath = join(serviceDir, `${nameCapitalized}Service.ts`);
await Bun.$`mkdir -p ${serviceDir}`.quiet();
@@ -84,7 +95,7 @@ const handler = async (args: string[]): Promise => {
// Auto-register in module router (named after last segment, e.g. UserRouter.ts)
const routerPath = join(
cwd,
- 'src',
+ cfg.srcDir || 'src',
'modules',
module,
`${moduleCapitalized}Router.ts`,
diff --git a/packages/kame/src/commands/registry.ts b/packages/kame/src/commands/registry.ts
index ff0aae8..578b5ca 100644
--- a/packages/kame/src/commands/registry.ts
+++ b/packages/kame/src/commands/registry.ts
@@ -1,6 +1,9 @@
+import type { KameConfig } from '../repl';
+
export type CommandHandler = (
args: string[],
flags: Record,
+ appConfig: KameConfig,
) => Promise | void;
export class Command {
diff --git a/packages/kame/src/index.ts b/packages/kame/src/index.ts
index 2b0d178..05d8742 100644
--- a/packages/kame/src/index.ts
+++ b/packages/kame/src/index.ts
@@ -1,8 +1,11 @@
export { startKame } from './repl';
import { Gaman } from 'gaman';
-import { startKame } from './repl';
+import { startKame, type KameConfig } from './repl';
-export function startKameWithGaman(gaman: Gaman) {
- return startKame();
+export function startKameWithGaman(
+ gaman: Gaman,
+ cfg: KameConfig = { srcDir: 'src' },
+) {
+ return startKame(cfg);
}
diff --git a/packages/kame/src/repl.ts b/packages/kame/src/repl.ts
index 7e15a03..178d265 100644
--- a/packages/kame/src/repl.ts
+++ b/packages/kame/src/repl.ts
@@ -12,10 +12,23 @@ import './commands/gen-controller';
import './commands/gen-service';
import './commands/gen-middleware';
import './commands/gen-exception';
+import './commands/gen-migration';
+import './commands/gen-seeder';
+
+import './commands/database/migrate';
+import './commands/database/seed';
+
import './commands/buntest-cmd';
import './commands/fetch';
-export function startKame() {
+export interface KameConfig {
+ /**
+ * @default 'src/'
+ */
+ srcDir?: string;
+}
+
+export function startKame(cfg: KameConfig = { srcDir: 'src' }) {
if (!process.env.KAME_CLI) return;
Logger.info(
`${TextFormat.BG_CYAN} ${TextFormat.BOLD}Kame ${TextFormat.RESET} System active. Type "help" for commands.`,
@@ -55,7 +68,7 @@ export function startKame() {
`Unknown command: "${commandName}". Run "help" to see available commands.`,
);
} else {
- await cmd.getHandler()(args, flags);
+ await cmd.getHandler()(args, flags, cfg);
}
rl.prompt();
diff --git a/packages/kame/src/templates/database.ts b/packages/kame/src/templates/database.ts
new file mode 100644
index 0000000..6c3c951
--- /dev/null
+++ b/packages/kame/src/templates/database.ts
@@ -0,0 +1,67 @@
+export const migrationTemplate = () => {
+ return `
+import { composeMigration } from "@gaman/db"
+
+/**
+ * This migration file acts as version control for your database schema.
+ * Use it to define changes such as creating tables, adding columns, or managing indexes.
+ */
+export default composeMigration({
+
+ /**
+ * The 'up' method is executed when you run the 'db:migrate' command.
+ * This is where you write the logic to APPLY changes to the database.
+ *
+ * Example:
+ * await m.createTable('users', (table) => {
+ * table.int('id').primary().autoIncrement();
+ * table.string('username').unique();
+ * table.text('bio');
+ * });
+ */
+ async up(m) {
+ // Write your 'up' logic here
+ },
+
+ /**
+ * The 'down' method is executed when you run the 'db:rollback' or 'db:migrate -fresh' command.
+ * This is where you write the logic to REVERSE the changes made in the 'up' method.
+ * Warning: Rolling back often results in permanent data loss in the affected tables.
+ *
+ * Example:
+ * await m.dropTable('users');
+ */
+ async down(m) {
+ // Write your 'down' logic here (the inverse of 'up')
+ },
+});
+`.trim();
+};
+
+export const seederTemplate = () => {
+ return `
+import { composeSeeder } from '@gaman/db';
+
+/**
+ * This seeder file is used to populate your database with initial or sample data.
+ * Useful for testing, development setup, or inserting default records.
+ */
+export default composeSeeder(async () => {
+ /**
+ * Add your seed data here.
+ *
+ * Example:
+ * await UserModel.create({
+ * name: 'Anomali',
+ * umur: 12,
+ * });
+ *
+ * You can also insert multiple records:
+ *
+ * await UserModel.createMany([
+ * { name: 'Anomali', umur: 12 },
+ * { name: 'Budi', umur: 20 },
+ * ]);
+ */
+});`.trim();
+};
diff --git a/src-test/console/index.ts b/src-test/console/index.ts
new file mode 100644
index 0000000..f63ec19
--- /dev/null
+++ b/src-test/console/index.ts
@@ -0,0 +1,10 @@
+import { Logger } from '../../dist/utils/logger';
+import { composeConsole } from '../../packages/kame/src/compose';
+
+export default composeConsole((kame) => {
+ kame
+ .command('ping', (args, flags) => {
+ Logger.info('pong');
+ })
+ .usage('ping ');
+});
diff --git a/src-test/database/migrations/20260504_221008_user-init.ts b/src-test/database/migrations/20260504_221008_user-init.ts
new file mode 100644
index 0000000..4c319b7
--- /dev/null
+++ b/src-test/database/migrations/20260504_221008_user-init.ts
@@ -0,0 +1,34 @@
+import { composeMigration } from '../../../packages/db/src/index';
+
+/**
+ * This migration file acts as version control for your database schema.
+ * Use it to define changes such as creating tables, adding columns, or managing indexes.
+ */
+export default composeMigration({
+ /**
+ * The 'up' method is executed when you run the 'db:migrate' command.
+ * This is where you write the logic to APPLY changes to the database.
+ *
+ * Example:
+ * await m.createTable('users', (table) => {
+ * table.int('id').primary().autoIncrement();
+ * table.string('username').unique();
+ * table.text('bio');
+ * });
+ */
+ async up(m) {
+ // Write your 'up' logic here
+ },
+
+ /**
+ * The 'down' method is executed when you run the 'db:rollback' or 'db:migrate -fresh' command.
+ * This is where you write the logic to REVERSE the changes made in the 'up' method.
+ * Warning: Rolling back often results in permanent data loss in the affected tables.
+ *
+ * Example:
+ * await m.dropTable('users');
+ */
+ async down(m) {
+ // Write your 'down' logic here (the inverse of 'up')
+ },
+});
diff --git a/src-test/database/migrations/20260504_221042_user-init.ts b/src-test/database/migrations/20260504_221042_user-init.ts
new file mode 100644
index 0000000..eb534df
--- /dev/null
+++ b/src-test/database/migrations/20260504_221042_user-init.ts
@@ -0,0 +1,35 @@
+import { composeMigration } from '../../../packages/db/src/index';
+
+/**
+ * This migration file acts as version control for your database schema.
+ * Use it to define changes such as creating tables, adding columns, or managing indexes.
+ */
+export default composeMigration({
+
+ /**
+ * The 'up' method is executed when you run the 'db:migrate' command.
+ * This is where you write the logic to APPLY changes to the database.
+ *
+ * Example:
+ * await m.createTable('users', (table) => {
+ * table.int('id').primary().autoIncrement();
+ * table.string('username').unique();
+ * table.text('bio');
+ * });
+ */
+ async up(m) {
+ // Write your 'up' logic here
+ },
+
+ /**
+ * The 'down' method is executed when you run the 'db:rollback' or 'db:migrate -fresh' command.
+ * This is where you write the logic to REVERSE the changes made in the 'up' method.
+ * Warning: Rolling back often results in permanent data loss in the affected tables.
+ *
+ * Example:
+ * await m.dropTable('users');
+ */
+ async down(m) {
+ // Write your 'down' logic here (the inverse of 'up')
+ },
+});
diff --git a/src-test/database/models/UserModel.ts b/src-test/database/models/UserModel.ts
new file mode 100644
index 0000000..9de2228
--- /dev/null
+++ b/src-test/database/models/UserModel.ts
@@ -0,0 +1,11 @@
+import { composeSchema, Model } from '../../../packages/db/src';
+
+export const UserSchema = composeSchema('users', (c) => ({
+ id: c.int().primary().autoIncrement(),
+ name: c.string(),
+ umur: c.int(),
+}));
+
+export type User = typeof UserSchema.infer;
+
+export default new Model(UserSchema);
diff --git a/src-test/database/seeders/TestUserSeeder.ts b/src-test/database/seeders/TestUserSeeder.ts
new file mode 100644
index 0000000..3af0468
--- /dev/null
+++ b/src-test/database/seeders/TestUserSeeder.ts
@@ -0,0 +1,24 @@
+import { composeSeeder } from '@gaman/db';
+
+/**
+ * This seeder file is used to populate your database with initial or sample data.
+ * Useful for testing, development setup, or inserting default records.
+ */
+export default composeSeeder(async () => {
+ /**
+ * Add your seed data here.
+ *
+ * Example:
+ * await UserModel.create({
+ * name: 'Anomali',
+ * umur: 12,
+ * });
+ *
+ * You can also insert multiple records:
+ *
+ * await UserModel.createMany([
+ * { name: 'Anomali', umur: 12 },
+ * { name: 'Budi', umur: 20 },
+ * ]);
+ */
+});
diff --git a/src-test/database/seeders/UserSeeder.ts b/src-test/database/seeders/UserSeeder.ts
new file mode 100644
index 0000000..7307f8b
--- /dev/null
+++ b/src-test/database/seeders/UserSeeder.ts
@@ -0,0 +1,9 @@
+import { composeSeeder } from '../../../packages/db/src';
+import UserModel from '../models/UserModel';
+
+export default composeSeeder(async () => {
+ UserModel.create({
+ name: 'Anomali',
+ umur: 12,
+ });
+});
diff --git a/src-test/index.ts b/src-test/index.ts
new file mode 100644
index 0000000..2591659
--- /dev/null
+++ b/src-test/index.ts
@@ -0,0 +1,9 @@
+import { defineBootstrap } from 'gaman';
+import { startKameWithGaman } from '../packages/kame/src';
+import AppRouter from './modules/app/AppRouter';
+defineBootstrap((app) => {
+ app.mount(AppRouter('/'));
+
+ app.mountServer({ http: 3431 });
+ startKameWithGaman(app, { srcDir: 'src-test' });
+});
diff --git a/src-test/modules/app/AppRouter.ts b/src-test/modules/app/AppRouter.ts
new file mode 100644
index 0000000..289dbe8
--- /dev/null
+++ b/src-test/modules/app/AppRouter.ts
@@ -0,0 +1,13 @@
+import { composeRouter } from 'gaman/compose';
+import AppController from './controllers/AppController';
+import { AppService } from './services/AppService';
+
+export default composeRouter((r) => {
+ r.mountService({
+ appService: AppService(),
+ });
+
+ r.get('/', [AppController, 'index']);
+ r.get('/sync', [AppController, 'sync']);
+ r.post('/create', [AppController, 'createUser']);
+});
diff --git a/src-test/modules/app/controllers/AppController.ts b/src-test/modules/app/controllers/AppController.ts
new file mode 100644
index 0000000..0d7f7ec
--- /dev/null
+++ b/src-test/modules/app/controllers/AppController.ts
@@ -0,0 +1,36 @@
+import { composeController } from 'gaman/compose';
+import { Res } from 'gaman/responder';
+import { AppService } from '../services/AppService';
+import UserModel from '../../../database/models/UserModel';
+
+export type Deps = {
+ appService: AppService;
+};
+
+export default composeController(({ appService }: Deps) => {
+ // TODO: Implement your controller logic here
+
+ return {
+ async index(ctx) {
+ const res = await UserModel.where('umur', '>=', 0).paginate(2, 2)
+ return Res.json(res);
+ },
+ async sync(ctx) {
+ await UserModel.sync();
+ return Res.json({
+ message: 'berhasil membuat table',
+ });
+ },
+ async createUser({ json }) {
+ const { name, umur } = await json();
+ const res = await UserModel.create({
+ name,
+ umur,
+ });
+
+ return Res.json({
+ message: 'berhasil di buat',
+ });
+ },
+ };
+});
diff --git a/src-test/modules/app/services/AppService.ts b/src-test/modules/app/services/AppService.ts
new file mode 100644
index 0000000..0837274
--- /dev/null
+++ b/src-test/modules/app/services/AppService.ts
@@ -0,0 +1,15 @@
+import { composeService } from 'gaman/compose';
+import type { RT } from 'gaman/types';
+
+export const AppService = composeService(() => {
+
+ // TODO: Implement your service logic here
+
+ return {
+ WelcomeMessage() {
+ return 'Welcome to App Service!';
+ },
+ };
+});
+
+export type AppService = RT;
diff --git a/tsconfig.json b/tsconfig.json
index 8c1f694..e665d10 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,64 +1,40 @@
{
- "compilerOptions": {
- "lib": [
- "ESNext"
- ],
- "target": "ESNext",
- "module": "Preserve",
- "moduleDetection": "force",
- "jsx": "react-jsx",
- "allowJs": true,
- "types": [
- "bun-types"
- ],
- "outDir": "./dist",
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "noEmit": true,
- "strict": true,
- "skipLibCheck": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedIndexedAccess": true,
- "noImplicitOverride": true,
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "noPropertyAccessFromIndexSignature": false,
- "allowSyntheticDefaultImports": true,
- "baseUrl": ".",
- "paths": {
- "gaman": [
- "./src/index.ts"
- ],
- "gaman/types": [
- "./src/types.ts"
- ],
- "gaman/responder": [
- "./src/responder.ts"
- ],
- "gaman/compose": [
- "./src/compose/index.ts"
- ],
- "gaman/utils": [
- "./src/utils/index.ts"
- ],
- "gaman/formdata": [
- "./src/context/formdata/index.ts"
- ],
- "gaman/header": [
- "./src/context/header/index.ts"
- ],
- "gaman/enums": [
- "./src/enums/index.ts"
- ]
- }
- },
- "include": [
- "src/**/*"
-, "test/console.ts" ],
- "exclude": [
- "**/*.test.ts",
- "tests",
- "benchmark"
- ]
-}
\ No newline at end of file
+ "compilerOptions": {
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "types": ["bun-types"],
+ "outDir": "./dist",
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+ "allowSyntheticDefaultImports": true,
+ "baseUrl": ".",
+ "paths": {
+ "gaman": ["./src/index.ts"],
+ "gaman/types": ["./src/types.ts"],
+ "gaman/responder": ["./src/responder.ts"],
+ "gaman/compose": ["./src/compose/index.ts"],
+ "gaman/utils": ["./src/utils/index.ts"],
+ "gaman/formdata": ["./src/context/formdata/index.ts"],
+ "gaman/header": ["./src/context/header/index.ts"],
+ "gaman/enums": ["./src/enums/index.ts"],
+ "@gaman/db": ["./packages/db/src/index.ts"],
+ "@gaman/db/migration": ["./packages/db/src/migration/index.ts"]
+ }
+ },
+ "include": ["src/**/*", "test/console.ts"],
+ "exclude": ["**/*.test.ts", "tests", "benchmark"]
+}