From 12c0f457fc74286638c1752a8ab5c51250159fcb Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Tue, 21 Apr 2026 20:53:10 +0700 Subject: [PATCH 01/22] feat: initialize @gaman/db --- bun.lock | 85 ++++++++++++- package.json | 5 + packages/db/.npmignore | 4 + packages/db/README.md | 1 + packages/db/package.json | 45 +++++++ packages/db/src/blueprint.ts | 34 +++++ packages/db/src/index.ts | 17 +++ packages/db/src/model.ts | 231 ++++++++++++++++++++++++++++++++++ packages/db/src/schema.ts | 17 +++ packages/db/tsconfig.dts.json | 60 +++++++++ packages/db/tsconfig.json | 8 ++ packages/kame/README.md | 22 +--- 12 files changed, 506 insertions(+), 23 deletions(-) create mode 100644 packages/db/.npmignore create mode 100644 packages/db/README.md create mode 100644 packages/db/package.json create mode 100644 packages/db/src/blueprint.ts create mode 100644 packages/db/src/index.ts create mode 100644 packages/db/src/model.ts create mode 100644 packages/db/src/schema.ts create mode 100644 packages/db/tsconfig.dts.json create mode 100644 packages/db/tsconfig.json diff --git a/bun.lock b/bun.lock index ec9582a..412f4be 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", @@ -24,11 +29,27 @@ }, "packages/cors": { "name": "@gaman/cors", - "version": "1.0.9", + "version": "1.1.0", + }, + "packages/db": { + "name": "@gaman/db", + "version": "0.1.0", + "devDependencies": { + "kysely": "^0.28.16", + "kysely-bun-sqlite": "^0.4.0", + }, + "peerDependencies": { + "kysely": ">=0.28.16", + "kysely-bun-sqlite": ">=0.4.0", + }, + }, + "packages/kame": { + "name": "@gaman/kame", + "version": "0.1.0", }, "packages/static": { "name": "@gaman/static", - "version": "1.0.6", + "version": "1.0.7", }, }, "packages": { @@ -92,6 +113,10 @@ "@gaman/cors": ["@gaman/cors@workspace:packages/cors"], + "@gaman/db": ["@gaman/db@workspace:packages/db"], + + "@gaman/kame": ["@gaman/kame@workspace:packages/kame"], + "@gaman/michi": ["@gaman/michi@0.1.3", "", {}, "sha512-vNkpTEfUa9F8KkpU1OwuY8mKEYmFcpILirsAD7S31IiEThx9inADI/ASO612miX1K6oGWOTqbNXAO09j5I1Hug=="], "@gaman/static": ["@gaman/static@workspace:packages/static"], @@ -212,6 +237,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=="], @@ -226,6 +253,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=="], @@ -246,6 +275,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=="], @@ -270,12 +301,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=="], @@ -306,20 +347,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=="], @@ -334,6 +399,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=="], @@ -350,6 +423,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=="], @@ -358,6 +433,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=="], @@ -386,6 +465,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..774db88 100644 --- a/package.json +++ b/package.json @@ -76,11 +76,16 @@ }, "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..38d5ad0 --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,45 @@ +{ + "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" + } + }, + "keywords": [ + "gamanjs", + "gaman", + "database", + "gaman db", + "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/blueprint.ts b/packages/db/src/blueprint.ts new file mode 100644 index 0000000..6d8ca1a --- /dev/null +++ b/packages/db/src/blueprint.ts @@ -0,0 +1,34 @@ +export class ColumnBuilder { + public config = { + type: '' as string, + nullable: false, + unsigned: false, + isPrimary: false, + isAutoIncrement: false, + isUnique: false, + isIndex: false, + defaultValue: null as any, + }; + + //? Properti bayangan untuk inferensi tipe data TypeScript + public _type!: T; + + int() { this.config.type = 'integer'; return this as ColumnBuilder; } + string(len = 255) { this.config.type = `varchar(${len})`; return this as ColumnBuilder; } + text() { this.config.type = 'text'; return this as ColumnBuilder; } + timestamp() { this.config.type = 'datetime'; return this as ColumnBuilder; } + + primary() { this.config.isPrimary = true; return this; } + autoIncrement() { this.config.isAutoIncrement = true; return this; } + nullable() { this.config.nullable = true; return this as ColumnBuilder; } + unique() { this.config.isUnique = true; return this; } + default(val: T) { this.config.defaultValue = val; return this; } +} + +export const col = { + id: () => new ColumnBuilder().int().primary().autoIncrement(), + string: (len?: number) => new ColumnBuilder().string(len), + int: () => new ColumnBuilder().int(), + text: () => new ColumnBuilder().text(), + timestamp: () => new ColumnBuilder().timestamp(), +}; \ No newline at end of file diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 0000000..740b9d0 --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,17 @@ +import { col } from './blueprint'; +import { Model } from './model'; +import { composeSchema } from './schema'; + +const UserSchema = composeSchema('user', { + id: col.id(), + name: col.string(), +}); + +export const User = new Model(UserSchema); +// await User.sync(); +// User.getRawKysely() +// await User.create({ +// name: 'yoga' +// }) +const user = await User.all(); +console.log(user) \ No newline at end of file diff --git a/packages/db/src/model.ts b/packages/db/src/model.ts new file mode 100644 index 0000000..7be48e1 --- /dev/null +++ b/packages/db/src/model.ts @@ -0,0 +1,231 @@ +import { Kysely, MysqlDialect, PostgresDialect } from 'kysely'; +import { BunSqliteDialect } from 'kysely-bun-sqlite'; +import { Database } from 'bun:sqlite'; +import type { composeSchema } from './schema'; +import type { ComparisonOperatorExpression } from 'kysely'; + +export class Model> { + public db: Kysely; + + constructor(public schema: T) { + const connection = process.env.DB_CONNECTION || 'sqlite'; + + this.db = new Kysely({ + dialect: this.getDialect(connection), + }); + } + + //! init table + async sync() { + let tableBuilder = this.db.schema + .createTable(this.schema.name) + .ifNotExists(); + + for (const [colName, colBuilder] of Object.entries(this.schema.fields)) { + const { config } = colBuilder as any; + tableBuilder = tableBuilder.addColumn(colName, config.type, (cb) => { + let res = cb; + if (config.isPrimary) res = res.primaryKey(); + if (config.isAutoIncrement) res = res.autoIncrement(); + if (config.isUnique) res = res.unique(); + if (!config.nullable) res = res.notNull(); + if (config.defaultValue !== null) + res = res.defaultTo(config.defaultValue); + return res; + }); + } + + return await tableBuilder.execute(); + } + + private getDialect(connection: string) { + 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 || '', + }; + + 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), + }); + } + } + + async find(id: number | string) { + return await this.db + .selectFrom(this.schema.name) + .selectAll() + .where('id', '=', id as any) + .executeTakeFirst(); + } + + async where( + column: keyof T['infer'], + op: ComparisonOperatorExpression, + value: any, + ) { + return this.db.selectFrom(this.schema.name).where(column as any, op, value); + } + + async create(data: Partial) { + return await this.db + .insertInto(this.schema.name) + .values(data as any) + .executeTakeFirstOrThrow(); + } + + /** + * @ID Mengambil semua data dari tabel. + * @EN Fetches all records from the table. + */ + async all(): Promise> { + return await this.db.selectFrom(this.schema.name).selectAll().execute(); + } + + /** + * @ID Mencari satu rekaman berdasarkan nama kolom dan nilai tertentu. + * @EN Finds a single record by a specific column and value. + */ + async findBy(column: keyof T['infer'], value: any): Promise { + return await this.db + .selectFrom(this.schema.name) + .selectAll() + .where(column as any, '=', value) + .executeTakeFirst() as any; + } + + /** + * @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.db + .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.db + .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.db + .selectFrom(this.schema.name) + .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.db + .selectFrom(this.schema.name) + .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.db + .selectFrom(this.schema.name) + .selectAll() + .limit(limit) + .offset(offset) + .execute(), + this.count(), + ]); + + return { + data, + meta: { + total, + page, + limit, + lastPage: Math.ceil(total / limit), + }, + }; + } + + /** + * @ID Mengembalikan instance query builder Kysely untuk kueri kustom. + * @EN Returns the Kysely query builder instance for custom queries. + */ + query() { + return this.db.selectFrom(this.schema.name); + } + + /** + * @ID Mengembalikan instance Kysely utama untuk akses database tingkat rendah. + * @EN Returns the raw Kysely instance for low-level database access. + */ + getRaw() { + return this.db; + } + + /** + * @ID Menghapus semua data di dalam tabel (bersihkan tabel). + * @EN Deletes all data within the table (clears the table). + */ + async truncate() { + return await this.db.deleteFrom(this.schema.name).execute(); + } +} diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts new file mode 100644 index 0000000..86a669d --- /dev/null +++ b/packages/db/src/schema.ts @@ -0,0 +1,17 @@ +import { type ColumnBuilder } from './blueprint'; + +export type SchemaDefinition = Record; + +export function composeSchema( + name: string, + fields: T, +) { + return { + name, + fields, + //! Magic: Mengambil tipe data asli dari tiap ColumnBuilder + infer: {} as { + [K in keyof T]: T[K]['_type']; + }, + }; +} 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 From df308dc921afa168f27b9429ddafa42dac6d8827 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 21:47:19 +0700 Subject: [PATCH 02/22] feat(kame): add srcDir config --- packages/kame/src/commands/gen-controller.ts | 15 +++++++++++++-- packages/kame/src/commands/gen-exception.ts | 15 +++++++++++++-- packages/kame/src/commands/gen-middleware.ts | 9 +++++++-- packages/kame/src/commands/gen-module.ts | 9 +++++++-- packages/kame/src/commands/gen-router.ts | 9 +++++++-- packages/kame/src/commands/gen-service.ts | 17 ++++++++++++++--- packages/kame/src/commands/registry.ts | 3 +++ packages/kame/src/index.ts | 9 ++++++--- packages/kame/src/repl.ts | 11 +++++++++-- 9 files changed, 79 insertions(+), 18 deletions(-) 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-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-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..703364e 100644 --- a/packages/kame/src/repl.ts +++ b/packages/kame/src/repl.ts @@ -15,7 +15,14 @@ import './commands/gen-exception'; 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 +62,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(); From 73638dd657cec6570ec526046cdab394fb380c14 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 22:23:22 +0700 Subject: [PATCH 03/22] feat(db): add composeSchema and composeSeeder --- packages/db/src/compose/index.ts | 2 ++ packages/db/src/compose/schema.ts | 20 ++++++++++++++++++++ packages/db/src/compose/seeder.ts | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 packages/db/src/compose/index.ts create mode 100644 packages/db/src/compose/schema.ts create mode 100644 packages/db/src/compose/seeder.ts diff --git a/packages/db/src/compose/index.ts b/packages/db/src/compose/index.ts new file mode 100644 index 0000000..8784e2b --- /dev/null +++ b/packages/db/src/compose/index.ts @@ -0,0 +1,2 @@ +export * from './schema'; +export * from './seeder'; diff --git a/packages/db/src/compose/schema.ts b/packages/db/src/compose/schema.ts new file mode 100644 index 0000000..4d3b182 --- /dev/null +++ b/packages/db/src/compose/schema.ts @@ -0,0 +1,20 @@ +import { type ColumnBuilder } from '../column-builder'; +import { RelationBuilder } from '../relation-builder'; + +export type SchemaDefinition = Record; + +export function composeSchema( + name: string, + fields: T, + relations?: (r: RelationBuilder) => any, +) { + return { + name, + fields, + relations: relations?.(new RelationBuilder(name)), + //! Magic: Mengambil tipe data asli dari tiap ColumnBuilder + infer: {} as { + [K in keyof T]: T[K]['_type']; + }, + }; +} diff --git a/packages/db/src/compose/seeder.ts b/packages/db/src/compose/seeder.ts new file mode 100644 index 0000000..17407c2 --- /dev/null +++ b/packages/db/src/compose/seeder.ts @@ -0,0 +1,5 @@ +export type SeederHandler = () => Promise | void; + +export function composeSeeder(seederName: string, handler: SeederHandler) { + return handler; +} From 7787176d65a45f50708bfc383e16a05f44982230 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 22:24:46 +0700 Subject: [PATCH 04/22] feat(db): change per model per connection to singleton connection --- packages/db/src/create-db.ts | 66 ++++++++++++ packages/db/src/model.ts | 196 +++++++++++++++++------------------ 2 files changed, 159 insertions(+), 103 deletions(-) create mode 100644 packages/db/src/create-db.ts diff --git a/packages/db/src/create-db.ts b/packages/db/src/create-db.ts new file mode 100644 index 0000000..ea5a886 --- /dev/null +++ b/packages/db/src/create-db.ts @@ -0,0 +1,66 @@ +// src/database/db.ts +import { Kysely, MysqlDialect, PostgresDialect } from 'kysely'; +import { BunSqliteDialect } from 'kysely-bun-sqlite'; +import { Database } from 'bun:sqlite'; + +let db: Kysely | null = null; + +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/model.ts b/packages/db/src/model.ts index 7be48e1..5062acc 100644 --- a/packages/db/src/model.ts +++ b/packages/db/src/model.ts @@ -1,21 +1,23 @@ -import { Kysely, MysqlDialect, PostgresDialect } from 'kysely'; -import { BunSqliteDialect } from 'kysely-bun-sqlite'; -import { Database } from 'bun:sqlite'; -import type { composeSchema } from './schema'; +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> { - public db: Kysely; + /** + * @ID Instance database (singleton). + * @EN Singleton database instance. + */ + public db: Kysely = getDB(); - constructor(public schema: T) { - const connection = process.env.DB_CONNECTION || 'sqlite'; + constructor(public schema: T) {} - this.db = new Kysely({ - dialect: this.getDialect(connection), - }); - } - - //! init table + //! 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.db.schema .createTable(this.schema.name) @@ -23,14 +25,18 @@ export class Model> { for (const [colName, colBuilder] of Object.entries(this.schema.fields)) { const { config } = colBuilder as any; + tableBuilder = tableBuilder.addColumn(colName, config.type, (cb) => { let res = cb; + if (config.isPrimary) res = res.primaryKey(); if (config.isAutoIncrement) res = res.autoIncrement(); if (config.isUnique) res = res.unique(); if (!config.nullable) res = res.notNull(); - if (config.defaultValue !== null) + if (config.defaultValue !== null) { res = res.defaultTo(config.defaultValue); + } + return res; }); } @@ -38,95 +44,94 @@ export class Model> { return await tableBuilder.execute(); } - private getDialect(connection: string) { - 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 || '', - }; + /** + * @ID Base query builder untuk tabel ini. + * @EN Base query builder for this table. + */ + query() { + return this.db.selectFrom(this.schema.name); + } - 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), - }); - } + /** + * @ID Mengambil semua data dari tabel. + * @EN Fetches all records from the table. + */ + async all(): Promise> { + return await this.query().selectAll().execute(); } - async find(id: number | string) { - return await this.db - .selectFrom(this.schema.name) + /** + * @ID Mencari banyak data berdasarkan ID (array). + * @EN Finds multiple records by ID (returns array). + */ + async find(id: number | string): Promise> { + return await this.query() .selectAll() - .where('id', '=', id as any) - .executeTakeFirst(); + .where('id' as any, '=', id as any) + .execute(); } - async where( + /** + * @ID Mencari satu rekaman berdasarkan nama kolom dan nilai tertentu. + * @EN Finds multiple record by a specific column and value. + */ + async findBy( column: keyof T['infer'], - op: ComparisonOperatorExpression, value: any, - ) { - return this.db.selectFrom(this.schema.name).where(column as any, op, value); + ): Promise> { + return await this.query() + .selectAll() + .where(column as any, '=', value) + .execute(); } - async create(data: Partial) { - return await this.db - .insertInto(this.schema.name) - .values(data as any) - .executeTakeFirstOrThrow(); + /** + * @ID Mencari satu data berdasarkan ID. + * @EN Finds a single record by ID. + */ + async findOne(id: number | string): Promise { + return await this.query() + .selectAll() + .where('id' as any, '=', id as any) + .executeTakeFirst(); } /** - * @ID Mengambil semua data dari tabel. - * @EN Fetches all records from the table. + * @ID Mencari satu data berdasarkan kolom. + * @EN Finds a single record by column. */ - async all(): Promise> { - return await this.db.selectFrom(this.schema.name).selectAll().execute(); + async findOneBy( + column: keyof T['infer'], + value: any, + ): Promise { + return await this.query() + .selectAll() + .where(column as any, '=', value) + .executeTakeFirst(); } /** - * @ID Mencari satu rekaman berdasarkan nama kolom dan nilai tertentu. - * @EN Finds a single record by a specific column and value. + * @ID Query filter berbasis kondisi (return query builder). + * @EN Conditional query filter (returns query builder). + */ + where( + column: keyof T['infer'], + op: ComparisonOperatorExpression, + value: any, + ) { + const qb = this.query().where(column as any, op, value); + return new Where(qb as any); + } + + /** + * @ID Membuat data baru ke tabel. + * @EN Inserts new record into table. */ - async findBy(column: keyof T['infer'], value: any): Promise { + async create(data: Partial) { return await this.db - .selectFrom(this.schema.name) - .selectAll() - .where(column as any, '=', value) - .executeTakeFirst() as any; + .insertInto(this.schema.name) + .values(data as any) + .executeTakeFirstOrThrow(); } /** @@ -138,7 +143,7 @@ export class Model> { .updateTable(this.schema.name) .set(data as any) .where('id' as any, '=', id as any) - .execute(); + .execute(); } /** @@ -157,8 +162,7 @@ export class Model> { * @EN Counts the total number of records in the table. */ async count() { - const result = await this.db - .selectFrom(this.schema.name) + const result = await this.query() .select((eb: any) => eb.fn.countAll().as('total')) .executeTakeFirst(); @@ -170,8 +174,7 @@ export class Model> { * @EN Retrieves the latest record ordered by a specific column. */ async latest(column: keyof T['infer'] = 'created_at' as any) { - return await this.db - .selectFrom(this.schema.name) + return await this.query() .selectAll() .orderBy(column as any, 'desc') .executeTakeFirst(); @@ -185,12 +188,7 @@ export class Model> { const offset = (page - 1) * limit; const [data, total] = await Promise.all([ - this.db - .selectFrom(this.schema.name) - .selectAll() - .limit(limit) - .offset(offset) - .execute(), + this.query().selectAll().limit(limit).offset(offset).execute(), this.count(), ]); @@ -205,14 +203,6 @@ export class Model> { }; } - /** - * @ID Mengembalikan instance query builder Kysely untuk kueri kustom. - * @EN Returns the Kysely query builder instance for custom queries. - */ - query() { - return this.db.selectFrom(this.schema.name); - } - /** * @ID Mengembalikan instance Kysely utama untuk akses database tingkat rendah. * @EN Returns the raw Kysely instance for low-level database access. From 7fe87a84b743ce7ed2f914f91d0954e42b836897 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 22:25:12 +0700 Subject: [PATCH 05/22] feat(db): add where builder --- packages/db/src/where.ts | 92 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 packages/db/src/where.ts diff --git a/packages/db/src/where.ts b/packages/db/src/where.ts new file mode 100644 index 0000000..737126a --- /dev/null +++ b/packages/db/src/where.ts @@ -0,0 +1,92 @@ +import type { SelectQueryBuilder } from 'kysely'; + +export class Where> { + constructor(private qb: SelectQueryBuilder) {} + + /** + * @ID Tambah kondisi where. + * @EN Adds where condition. + */ + where( + column: keyof T, + op: any, + value: any + ) { + this.qb = this.qb.where(column as string, op, value); + return this; + } + + /** + * @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 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), + }, + }; + } +} \ No newline at end of file From 5c82fa54143fe99ce7f72e37fa79aea326b86a90 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 22:26:26 +0700 Subject: [PATCH 06/22] feat(db): add relation builder --- .../src/{blueprint.ts => column-builder.ts} | 0 packages/db/src/index.ts | 20 ++----------- packages/db/src/relation-builder.ts | 29 +++++++++++++++++++ packages/db/src/schema.ts | 17 ----------- 4 files changed, 32 insertions(+), 34 deletions(-) rename packages/db/src/{blueprint.ts => column-builder.ts} (100%) create mode 100644 packages/db/src/relation-builder.ts delete mode 100644 packages/db/src/schema.ts diff --git a/packages/db/src/blueprint.ts b/packages/db/src/column-builder.ts similarity index 100% rename from packages/db/src/blueprint.ts rename to packages/db/src/column-builder.ts diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 740b9d0..e2dd457 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,17 +1,3 @@ -import { col } from './blueprint'; -import { Model } from './model'; -import { composeSchema } from './schema'; - -const UserSchema = composeSchema('user', { - id: col.id(), - name: col.string(), -}); - -export const User = new Model(UserSchema); -// await User.sync(); -// User.getRawKysely() -// await User.create({ -// name: 'yoga' -// }) -const user = await User.all(); -console.log(user) \ No newline at end of file +export { Model } from './model'; +export { col, ColumnBuilder } from './column-builder'; +export { composeSchema } from './compose/schema'; \ No newline at end of file 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/schema.ts b/packages/db/src/schema.ts deleted file mode 100644 index 86a669d..0000000 --- a/packages/db/src/schema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type ColumnBuilder } from './blueprint'; - -export type SchemaDefinition = Record; - -export function composeSchema( - name: string, - fields: T, -) { - return { - name, - fields, - //! Magic: Mengambil tipe data asli dari tiap ColumnBuilder - infer: {} as { - [K in keyof T]: T[K]['_type']; - }, - }; -} From 0c0af8438baec09dd1375b6ad7534385cf20bb62 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 22:27:25 +0700 Subject: [PATCH 07/22] docs: update gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 859834658c1ff220404a7604fb70abc435335544 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Fri, 24 Apr 2026 23:47:38 +0700 Subject: [PATCH 08/22] feat(db): change fields schema --- packages/db/src/compose/schema.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/db/src/compose/schema.ts b/packages/db/src/compose/schema.ts index 4d3b182..6431b26 100644 --- a/packages/db/src/compose/schema.ts +++ b/packages/db/src/compose/schema.ts @@ -1,20 +1,20 @@ -import { type ColumnBuilder } from '../column-builder'; +import { column, ColumnBuilder } from '../column-builder'; import { RelationBuilder } from '../relation-builder'; export type SchemaDefinition = Record; export function composeSchema( name: string, - fields: T, - relations?: (r: RelationBuilder) => any, + fields: (c: typeof column) => T, + relations?: (r: RelationBuilder) => any, ) { return { name, - fields, - relations: relations?.(new RelationBuilder(name)), + fields: fields(column), + 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 From 6bec2202035fd332584bfc767e668870618b15cf Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Sat, 2 May 2026 23:01:17 +0700 Subject: [PATCH 09/22] feat(db): add columns --- packages/db/src/column/base-column.ts | 8 +++ packages/db/src/column/defintion-column.ts | 60 ++++++++++++++++++ packages/db/src/column/index.ts | 4 ++ .../db/src/column/schema-column-builder.ts | 63 +++++++++++++++++++ packages/db/src/column/type-column.ts | 37 +++++++++++ 5 files changed, 172 insertions(+) create mode 100644 packages/db/src/column/base-column.ts create mode 100644 packages/db/src/column/defintion-column.ts create mode 100644 packages/db/src/column/index.ts create mode 100644 packages/db/src/column/schema-column-builder.ts create mode 100644 packages/db/src/column/type-column.ts 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; +} From a8cca0c6a86a4b9333ba2e0c339cd3ef1f5c7b4b Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Sat, 2 May 2026 23:01:26 +0700 Subject: [PATCH 10/22] feat(db): add migrations --- packages/db/src/migration/index.ts | 2 + .../db/src/migration/migration-builder.ts | 199 ++++++++++++++++++ .../src/migration/migration-column-builder.ts | 63 ++++++ 3 files changed, 264 insertions(+) create mode 100644 packages/db/src/migration/index.ts create mode 100644 packages/db/src/migration/migration-builder.ts create mode 100644 packages/db/src/migration/migration-column-builder.ts 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..af8a061 --- /dev/null +++ b/packages/db/src/migration/migration-builder.ts @@ -0,0 +1,199 @@ +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'; + +export class MigrationBuilder { + constructor(private kysely: Kysely) {} + + /** + * 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 !== 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'); + } +} From 3051b40ea450eefa67ff8a4f01935425741391b6 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Sat, 2 May 2026 23:02:02 +0700 Subject: [PATCH 11/22] feat(db): add composes --- packages/db/src/compose/index.ts | 1 + packages/db/src/compose/migration.ts | 8 ++++++++ packages/db/src/compose/schema.ts | 9 +++++---- packages/db/src/compose/seeder.ts | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 packages/db/src/compose/migration.ts diff --git a/packages/db/src/compose/index.ts b/packages/db/src/compose/index.ts index 8784e2b..dfc6b1c 100644 --- a/packages/db/src/compose/index.ts +++ b/packages/db/src/compose/index.ts @@ -1,2 +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..5a41213 --- /dev/null +++ b/packages/db/src/compose/migration.ts @@ -0,0 +1,8 @@ +import type { MigrationBuilder } from '../migration'; + +export function composeMigration(handlers: { + up: (m: MigrationBuilder) => Promise | void; + down: (m: MigrationBuilder) => Promise | void; +}) { + return handlers; +} \ No newline at end of file diff --git a/packages/db/src/compose/schema.ts b/packages/db/src/compose/schema.ts index 6431b26..84081f3 100644 --- a/packages/db/src/compose/schema.ts +++ b/packages/db/src/compose/schema.ts @@ -1,16 +1,17 @@ -import { column, ColumnBuilder } from '../column-builder'; +import type { ColumnDefinition } from '../column/defintion-column'; +import { SchemaColumnBuilder } from '../column/schema-column-builder'; import { RelationBuilder } from '../relation-builder'; -export type SchemaDefinition = Record; +export type SchemaDefinition = Record; export function composeSchema( name: string, - fields: (c: typeof column) => T, + fields: (c: SchemaColumnBuilder) => T, relations?: (r: RelationBuilder) => any, ) { return { name, - fields: fields(column), + fields: fields(new SchemaColumnBuilder()), relations: relations?.(new RelationBuilder(name)), //! Magic: Mengambil tipe data asli dari tiap ColumnBuilder infer: {} as { diff --git a/packages/db/src/compose/seeder.ts b/packages/db/src/compose/seeder.ts index 17407c2..23fdb24 100644 --- a/packages/db/src/compose/seeder.ts +++ b/packages/db/src/compose/seeder.ts @@ -1,5 +1,5 @@ export type SeederHandler = () => Promise | void; -export function composeSeeder(seederName: string, handler: SeederHandler) { +export function composeSeeder( handler: SeederHandler) { return handler; } From cea2caec2be70725de10fe190f74293ef2b12d36 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Sat, 2 May 2026 23:02:31 +0700 Subject: [PATCH 12/22] fix(db): model.ts --- packages/db/src/model.ts | 122 ++++++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/packages/db/src/model.ts b/packages/db/src/model.ts index 5062acc..1d69f33 100644 --- a/packages/db/src/model.ts +++ b/packages/db/src/model.ts @@ -9,7 +9,7 @@ export class Model> { * @ID Instance database (singleton). * @EN Singleton database instance. */ - public db: Kysely = getDB(); + public kysely: Kysely = getDB(); constructor(public schema: T) {} @@ -19,29 +19,58 @@ export class Model> { * @EN Creates table based on schema if it does not exist. */ async sync() { - let tableBuilder = this.db.schema + let tableBuilder = this.kysely.schema .createTable(this.schema.name) .ifNotExists(); - for (const [colName, colBuilder] of Object.entries(this.schema.fields)) { - const { config } = colBuilder as any; - - tableBuilder = tableBuilder.addColumn(colName, config.type, (cb) => { - let res = cb; + 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 !== null && + typeof colDefintion.defaultValue !== 'function' + ) { + res = res.defaultTo(colDefintion.defaultValue); + } + + return res; + }, + ); + + //? simpan index untuk dibuat nanti + if (colDefintion.isIndex) { + indexes.push(colName); + } + } - if (config.isPrimary) res = res.primaryKey(); - if (config.isAutoIncrement) res = res.autoIncrement(); - if (config.isUnique) res = res.unique(); - if (!config.nullable) res = res.notNull(); - if (config.defaultValue !== null) { - res = res.defaultTo(config.defaultValue); - } + const result = await tableBuilder.execute(); - return res; - }); + // 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 await tableBuilder.execute(); + return result; } /** @@ -49,7 +78,7 @@ export class Model> { * @EN Base query builder for this table. */ query() { - return this.db.selectFrom(this.schema.name); + return this.kysely.selectFrom(this.schema.name); } /** @@ -60,47 +89,22 @@ export class Model> { return await this.query().selectAll().execute(); } - /** - * @ID Mencari banyak data berdasarkan ID (array). - * @EN Finds multiple records by ID (returns array). - */ - async find(id: number | string): Promise> { - return await this.query() - .selectAll() - .where('id' as any, '=', id as any) - .execute(); - } - /** * @ID Mencari satu rekaman berdasarkan nama kolom dan nilai tertentu. * @EN Finds multiple record by a specific column and value. */ - async findBy( - column: keyof T['infer'], - value: any, - ): Promise> { + 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 ID. - * @EN Finds a single record by ID. - */ - async findOne(id: number | string): Promise { - return await this.query() - .selectAll() - .where('id' as any, '=', id as any) - .executeTakeFirst(); - } - /** * @ID Mencari satu data berdasarkan kolom. * @EN Finds a single record by column. */ - async findOneBy( + async findOne( column: keyof T['infer'], value: any, ): Promise { @@ -128,9 +132,23 @@ export class Model> { * @EN Inserts new record into table. */ async create(data: Partial) { - return await this.db + const finalData: any = { ...data }; + + for (const [key, col] of Object.entries(this.schema.fields)) { + const config = (col as any).config; + + if (finalData[key] === undefined && config.defaultValue !== null) { + if (typeof config.defaultValue === 'function') { + finalData[key] = config.defaultValue(); + } else { + finalData[key] = config.defaultValue; + } + } + } + + return await this.kysely .insertInto(this.schema.name) - .values(data as any) + .values(finalData) .executeTakeFirstOrThrow(); } @@ -139,11 +157,11 @@ export class Model> { * @EN Updates a record based on its unique ID. */ async update(id: number | string, data: Partial) { - return await this.db + return await this.kysely .updateTable(this.schema.name) .set(data as any) .where('id' as any, '=', id as any) - .execute(); + .execute(); } /** @@ -151,7 +169,7 @@ export class Model> { * @EN Deletes a record based on its unique ID. */ async delete(id: number | string) { - return await this.db + return await this.kysely .deleteFrom(this.schema.name) .where('id' as any, '=', id as any) .execute(); @@ -208,7 +226,7 @@ export class Model> { * @EN Returns the raw Kysely instance for low-level database access. */ getRaw() { - return this.db; + return this.kysely; } /** @@ -216,6 +234,6 @@ export class Model> { * @EN Deletes all data within the table (clears the table). */ async truncate() { - return await this.db.deleteFrom(this.schema.name).execute(); + return await this.kysely.deleteFrom(this.schema.name).execute(); } } From b7eef3fa1a2752a7455e0ec68d31cb2542806a25 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Sat, 2 May 2026 23:03:01 +0700 Subject: [PATCH 13/22] feat(db): clean up code --- packages/db/package.json | 11 +++ packages/db/src/column-builder.ts | 34 -------- packages/db/src/create-db.ts | 2 +- packages/db/src/index.ts | 3 +- packages/db/src/types.ts | 139 ++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 37 deletions(-) delete mode 100644 packages/db/src/column-builder.ts create mode 100644 packages/db/src/types.ts diff --git a/packages/db/package.json b/packages/db/package.json index 38d5ad0..7b2e4ef 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -9,6 +9,16 @@ "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": [ @@ -17,6 +27,7 @@ "database", "gaman db", "orm", + "gaman orm", "bun" ], "repository": { diff --git a/packages/db/src/column-builder.ts b/packages/db/src/column-builder.ts deleted file mode 100644 index 6d8ca1a..0000000 --- a/packages/db/src/column-builder.ts +++ /dev/null @@ -1,34 +0,0 @@ -export class ColumnBuilder { - public config = { - type: '' as string, - nullable: false, - unsigned: false, - isPrimary: false, - isAutoIncrement: false, - isUnique: false, - isIndex: false, - defaultValue: null as any, - }; - - //? Properti bayangan untuk inferensi tipe data TypeScript - public _type!: T; - - int() { this.config.type = 'integer'; return this as ColumnBuilder; } - string(len = 255) { this.config.type = `varchar(${len})`; return this as ColumnBuilder; } - text() { this.config.type = 'text'; return this as ColumnBuilder; } - timestamp() { this.config.type = 'datetime'; return this as ColumnBuilder; } - - primary() { this.config.isPrimary = true; return this; } - autoIncrement() { this.config.isAutoIncrement = true; return this; } - nullable() { this.config.nullable = true; return this as ColumnBuilder; } - unique() { this.config.isUnique = true; return this; } - default(val: T) { this.config.defaultValue = val; return this; } -} - -export const col = { - id: () => new ColumnBuilder().int().primary().autoIncrement(), - string: (len?: number) => new ColumnBuilder().string(len), - int: () => new ColumnBuilder().int(), - text: () => new ColumnBuilder().text(), - timestamp: () => new ColumnBuilder().timestamp(), -}; \ No newline at end of file diff --git a/packages/db/src/create-db.ts b/packages/db/src/create-db.ts index ea5a886..e8d3196 100644 --- a/packages/db/src/create-db.ts +++ b/packages/db/src/create-db.ts @@ -1,10 +1,10 @@ -// src/database/db.ts 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; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index e2dd457..516e04b 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,3 +1,2 @@ export { Model } from './model'; -export { col, ColumnBuilder } from './column-builder'; -export { composeSchema } from './compose/schema'; \ No newline at end of file +export * from './compose'; 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 From 073f579baeaa055b469b3e40fd3cf881490ca62e Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Sat, 2 May 2026 23:49:44 +0700 Subject: [PATCH 14/22] fix(db): migration-builder --- packages/db/src/migration/migration-builder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/db/src/migration/migration-builder.ts b/packages/db/src/migration/migration-builder.ts index af8a061..7ea7be9 100644 --- a/packages/db/src/migration/migration-builder.ts +++ b/packages/db/src/migration/migration-builder.ts @@ -3,9 +3,10 @@ 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 { - constructor(private kysely: Kysely) {} + private kysely = getDB(); /** * CREATE TABLE @@ -86,6 +87,7 @@ export class MigrationBuilder { if (colDefintion.isUnique) res = res.unique(); if (!colDefintion.isNullable) res = res.notNull(); if ( + colDefintion.defaultValue !== undefined && colDefintion.defaultValue !== null && typeof colDefintion.defaultValue !== 'function' ) { From 9247f335c2c312272cd4b40eb6dbe5dd18940fdd Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:08:53 +0700 Subject: [PATCH 15/22] feat(db): add template migration and seeder --- packages/kame/src/templates/database.ts | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/kame/src/templates/database.ts 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(); +}; From 6d2a5d7108de646d624374691d3f7f65abb3b891 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:10:16 +0700 Subject: [PATCH 16/22] feat(kame): new commands gen:migration and gen:seeder --- .../kame/src/commands/database/migrate.ts | 73 +++++++++++++++++++ packages/kame/src/commands/database/seed.ts | 25 +++++++ packages/kame/src/commands/gen-migration.ts | 62 ++++++++++++++++ packages/kame/src/commands/gen-seeder.ts | 46 ++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 packages/kame/src/commands/database/migrate.ts create mode 100644 packages/kame/src/commands/database/seed.ts create mode 100644 packages/kame/src/commands/gen-migration.ts create mode 100644 packages/kame/src/commands/gen-seeder.ts 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-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-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, +}); From 5d772438f7f4544f80577797831cad7194c8e2cf Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:11:38 +0700 Subject: [PATCH 17/22] feat(kame): repl --- packages/kame/src/repl.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/kame/src/repl.ts b/packages/kame/src/repl.ts index 703364e..178d265 100644 --- a/packages/kame/src/repl.ts +++ b/packages/kame/src/repl.ts @@ -12,6 +12,12 @@ 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'; From 7db3ac4f6909b21a0eb244abe2a9639fcb787193 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:13:39 +0700 Subject: [PATCH 18/22] fix(db): refactor composeMigration --- packages/db/src/compose/migration.ts | 48 +++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/db/src/compose/migration.ts b/packages/db/src/compose/migration.ts index 5a41213..eec73b8 100644 --- a/packages/db/src/compose/migration.ts +++ b/packages/db/src/compose/migration.ts @@ -1,8 +1,48 @@ -import type { MigrationBuilder } from '../migration'; +import { MigrationModel } from '../database/MigrationModel'; +import { MigrationBuilder } from '../migration'; +import { Logger } from 'gaman/utils'; -export function composeMigration(handlers: { +export async function composeMigration(handlers: { up: (m: MigrationBuilder) => Promise | void; down: (m: MigrationBuilder) => Promise | void; }) { - return handlers; -} \ No newline at end of file + 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}`); + }, + }; +} From 0b199c4e42e3aca1807beccd5986ad7df586ad79 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:14:01 +0700 Subject: [PATCH 19/22] feat(db): add migration model --- packages/db/src/database/MigrationModel.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/db/src/database/MigrationModel.ts 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); From 4b293205394de2978c5c1959e9a118cb021b5e29 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:33:16 +0700 Subject: [PATCH 20/22] feat(db): refactor models --- packages/db/src/model.ts | 56 +++++++++++++++++++++-------- packages/db/src/where.ts | 76 +++++++++++++++++++++++++++++++++------- 2 files changed, 105 insertions(+), 27 deletions(-) diff --git a/packages/db/src/model.ts b/packages/db/src/model.ts index 1d69f33..75b3adc 100644 --- a/packages/db/src/model.ts +++ b/packages/db/src/model.ts @@ -30,10 +30,8 @@ export class Model> { throw new Error(`Column "${colName}" has no type defined`); } - tableBuilder = tableBuilder.addColumn( - colName, - colDefintion.type, - (cb) => { + tableBuilder = tableBuilder + .addColumn(colName, colDefintion.type, (cb) => { let res = cb; if (colDefintion.isPrimary) res = res.primaryKey(); @@ -42,6 +40,7 @@ export class Model> { if (colDefintion.isUnique) res = res.unique(); if (!colDefintion.isNullable) res = res.notNull(); if ( + colDefintion.defaultValue !== undefined && colDefintion.defaultValue !== null && typeof colDefintion.defaultValue !== 'function' ) { @@ -49,8 +48,8 @@ export class Model> { } return res; - }, - ); + }) + .ifNotExists(); //? simpan index untuk dibuat nanti if (colDefintion.isIndex) { @@ -123,10 +122,12 @@ export class Model> { op: ComparisonOperatorExpression, value: any, ) { - const qb = this.query().where(column as any, op, value); - return new Where(qb as 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. @@ -135,13 +136,11 @@ export class Model> { const finalData: any = { ...data }; for (const [key, col] of Object.entries(this.schema.fields)) { - const config = (col as any).config; - - if (finalData[key] === undefined && config.defaultValue !== null) { - if (typeof config.defaultValue === 'function') { - finalData[key] = config.defaultValue(); + if (finalData[key] === undefined && col.defaultValue !== null) { + if (typeof col.defaultValue === 'function') { + finalData[key] = col.defaultValue(); } else { - finalData[key] = config.defaultValue; + finalData[key] = col.defaultValue; } } } @@ -152,6 +151,33 @@ export class Model> { .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. diff --git a/packages/db/src/where.ts b/packages/db/src/where.ts index 737126a..3df4166 100644 --- a/packages/db/src/where.ts +++ b/packages/db/src/where.ts @@ -1,29 +1,68 @@ -import type { SelectQueryBuilder } from 'kysely'; +import type { ComparisonOperatorExpression } from 'kysely'; +import type { Kysely, SelectQueryBuilder } from 'kysely'; + +export type WhereCondition = [keyof T, ComparisonOperatorExpression, any]; export class Where> { - constructor(private qb: SelectQueryBuilder) {} + 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: any, - value: any - ) { + 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' - ) { + orderBy(column: keyof T, direction: 'asc' | 'desc' = 'asc') { this.qb = this.qb.orderBy(column as string, direction); return this; } @@ -62,6 +101,19 @@ export class Where> { 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. @@ -89,4 +141,4 @@ export class Where> { }, }; } -} \ No newline at end of file +} From 0c79426d84fdd6672513a17120d525ec06845060 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:40:25 +0700 Subject: [PATCH 21/22] test: upload --- src-test/console/index.ts | 10 ++++++ .../migrations/20260504_221008_user-init.ts | 34 ++++++++++++++++++ .../migrations/20260504_221042_user-init.ts | 35 ++++++++++++++++++ src-test/database/models/UserModel.ts | 11 ++++++ src-test/database/seeders/TestUserSeeder.ts | 24 +++++++++++++ src-test/database/seeders/UserSeeder.ts | 9 +++++ src-test/index.ts | 9 +++++ src-test/modules/app/AppRouter.ts | 13 +++++++ .../modules/app/controllers/AppController.ts | 36 +++++++++++++++++++ src-test/modules/app/services/AppService.ts | 15 ++++++++ 10 files changed, 196 insertions(+) create mode 100644 src-test/console/index.ts create mode 100644 src-test/database/migrations/20260504_221008_user-init.ts create mode 100644 src-test/database/migrations/20260504_221042_user-init.ts create mode 100644 src-test/database/models/UserModel.ts create mode 100644 src-test/database/seeders/TestUserSeeder.ts create mode 100644 src-test/database/seeders/UserSeeder.ts create mode 100644 src-test/index.ts create mode 100644 src-test/modules/app/AppRouter.ts create mode 100644 src-test/modules/app/controllers/AppController.ts create mode 100644 src-test/modules/app/services/AppService.ts 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; From 677bb6bd074a26c63d48a2395d1614869f90c355 Mon Sep 17 00:00:00 2001 From: Angga7Togk Date: Mon, 18 May 2026 19:40:54 +0700 Subject: [PATCH 22/22] docs(db): clear --- README.md | 15 +++++--- package.json | 1 + tsconfig.json | 102 +++++++++++++++++++------------------------------- 3 files changed, 50 insertions(+), 68 deletions(-) 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 ✨ A table of avatars from the project's contributors +A table of avatars from the project's contributors ## Contributing diff --git a/package.json b/package.json index 774db88..8d4026d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "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" }, 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"] +}