create-nexgen 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/package.json +26 -0
  2. package/src/index.js +108 -0
  3. package/template/.dockerignore +14 -0
  4. package/template/.env +58 -0
  5. package/template/.env.example +59 -0
  6. package/template/.prettierignore +5 -0
  7. package/template/.prettierrc +8 -0
  8. package/template/README.md +447 -0
  9. package/template/drizzle.config.ts +29 -0
  10. package/template/eslint.config.js +52 -0
  11. package/template/gitignore-stub +24 -0
  12. package/template/package.json +96 -0
  13. package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
  14. package/template/public/assets/Button-_7aQ7gHL.js +1 -0
  15. package/template/public/assets/Input-CLNJXmKc.css +1 -0
  16. package/template/public/assets/Input-z8GI8Aqo.js +1 -0
  17. package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
  18. package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
  19. package/template/public/assets/Layout-DotR1sQC.js +1 -0
  20. package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
  21. package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
  22. package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
  23. package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  24. package/template/public/assets/dashboard-CwybEyLc.js +1 -0
  25. package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
  26. package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
  27. package/template/public/assets/index-Bleyx5dm.js +64 -0
  28. package/template/public/assets/index-DUw8E6Yg.css +1 -0
  29. package/template/public/assets/login-DC7PTlQF.js +1 -0
  30. package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
  31. package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
  32. package/template/public/assets/register-3O7Qs28C.js +1 -0
  33. package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
  34. package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
  35. package/template/public/index.html +17 -0
  36. package/template/src/database/migrations/mysql/0000_init.sql +73 -0
  37. package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
  38. package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
  39. package/template/src/database/schema.ts +4 -0
  40. package/template/src/env.ts +107 -0
  41. package/template/src/framework/cache/cache.ts +81 -0
  42. package/template/src/framework/database/connection.ts +168 -0
  43. package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
  44. package/template/src/framework/database/paginate.ts +200 -0
  45. package/template/src/framework/database/schema.ts +26 -0
  46. package/template/src/framework/database/seed.ts +33 -0
  47. package/template/src/framework/events/dispatcher.ts +57 -0
  48. package/template/src/framework/facade.ts +27 -0
  49. package/template/src/framework/http/app.ts +61 -0
  50. package/template/src/framework/http/cors.ts +19 -0
  51. package/template/src/framework/http/logger.ts +85 -0
  52. package/template/src/framework/http/openapi.ts +34 -0
  53. package/template/src/framework/http/ratelimiter.ts +13 -0
  54. package/template/src/framework/http/router.ts +76 -0
  55. package/template/src/framework/http/static.ts +33 -0
  56. package/template/src/framework/http/validation.ts +24 -0
  57. package/template/src/framework/kernel.ts +40 -0
  58. package/template/src/framework/maker-cli/src/index.mjs +51 -0
  59. package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
  60. package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
  61. package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
  62. package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
  63. package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
  64. package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
  65. package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
  66. package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
  67. package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
  68. package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
  69. package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
  70. package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
  71. package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
  72. package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
  73. package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
  74. package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
  75. package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
  76. package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
  77. package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
  78. package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
  79. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
  80. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
  81. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
  82. package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
  83. package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
  84. package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
  85. package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
  86. package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
  87. package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
  88. package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
  89. package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
  90. package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
  91. package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
  92. package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
  93. package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
  94. package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
  95. package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
  96. package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
  97. package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
  98. package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
  99. package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
  100. package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
  101. package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
  102. package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
  103. package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
  104. package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
  105. package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
  106. package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
  107. package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
  108. package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
  109. package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
  110. package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
  111. package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
  112. package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
  113. package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
  114. package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
  115. package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
  116. package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
  117. package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
  118. package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
  119. package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
  120. package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
  121. package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
  122. package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
  123. package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
  124. package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
  125. package/template/src/framework/modules/discover.ts +54 -0
  126. package/template/src/framework/modules/routes.ts +26 -0
  127. package/template/src/framework/notification/index.ts +109 -0
  128. package/template/src/framework/queue/clear.ts +20 -0
  129. package/template/src/framework/queue/queue.ts +213 -0
  130. package/template/src/framework/queue/ui.ts +104 -0
  131. package/template/src/framework/queue/worker.ts +33 -0
  132. package/template/src/framework/realtime/broadcast.ts +27 -0
  133. package/template/src/framework/realtime/index.ts +1 -0
  134. package/template/src/framework/realtime/socket-cookie.ts +65 -0
  135. package/template/src/framework/realtime/socket.ts +132 -0
  136. package/template/src/framework/realtime/types.ts +6 -0
  137. package/template/src/framework/realtime/ui.ts +16 -0
  138. package/template/src/framework/redis/client.ts +126 -0
  139. package/template/src/framework/scheduler/lock.ts +124 -0
  140. package/template/src/framework/scheduler/run.ts +26 -0
  141. package/template/src/framework/scheduler/scheduler.ts +82 -0
  142. package/template/src/framework/server.ts +147 -0
  143. package/template/src/framework/session/session.ts +116 -0
  144. package/template/src/framework/storage/storage.ts +743 -0
  145. package/template/src/framework/support/cookie.ts +78 -0
  146. package/template/src/framework/support/jwt.ts +45 -0
  147. package/template/src/framework/support/lifecycle.ts +35 -0
  148. package/template/src/framework/support/logger.ts +102 -0
  149. package/template/src/framework/support/mail.ts +43 -0
  150. package/template/src/framework/support/password.ts +23 -0
  151. package/template/src/framework/support/url.ts +25 -0
  152. package/template/src/middlewares/auth-middleware.ts +98 -0
  153. package/template/src/middlewares/role-middleware.ts +24 -0
  154. package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
  155. package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
  156. package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
  157. package/template/src/modules/auth/controllers/role.controller.ts +25 -0
  158. package/template/src/modules/auth/database/models/notifications.ts +22 -0
  159. package/template/src/modules/auth/database/models/role.ts +14 -0
  160. package/template/src/modules/auth/database/models/user.ts +46 -0
  161. package/template/src/modules/auth/database/seeders/role.ts +19 -0
  162. package/template/src/modules/auth/database/seeders/user.ts +33 -0
  163. package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
  164. package/template/src/modules/auth/jobs/registeruser.ts +31 -0
  165. package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
  166. package/template/src/modules/auth/routes/api.ts +151 -0
  167. package/template/src/modules/auth/routes/role.ts +39 -0
  168. package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
  169. package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
  170. package/template/src/modules/welcome/database/models/welcome.ts +6 -0
  171. package/template/src/modules/welcome/routes/api.ts +20 -0
  172. package/template/src/resources/index.html +16 -0
  173. package/template/src/resources/src/App.vue +5 -0
  174. package/template/src/resources/src/assets/css/styles.css +14934 -0
  175. package/template/src/resources/src/assets/css/styles.css.map +1 -0
  176. package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
  177. package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
  178. package/template/src/resources/src/assets/images/logo-1.png +0 -0
  179. package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
  180. package/template/src/resources/src/assets/images/logo-dark.png +0 -0
  181. package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
  182. package/template/src/resources/src/assets/images/logo-sm.png +0 -0
  183. package/template/src/resources/src/assets/images/logo1.png +0 -0
  184. package/template/src/resources/src/assets/images/logo2.png +0 -0
  185. package/template/src/resources/src/assets/scss/custom.css +217 -0
  186. package/template/src/resources/src/assets/scss/custom.css.map +1 -0
  187. package/template/src/resources/src/assets/scss/custom.scss +1100 -0
  188. package/template/src/resources/src/components/Button.vue +35 -0
  189. package/template/src/resources/src/components/Checkbox.vue +29 -0
  190. package/template/src/resources/src/components/FloatButton.vue +36 -0
  191. package/template/src/resources/src/components/Href.vue +32 -0
  192. package/template/src/resources/src/components/Input.vue +227 -0
  193. package/template/src/resources/src/components/InputGroup.vue +153 -0
  194. package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
  195. package/template/src/resources/src/components/Modal.vue +102 -0
  196. package/template/src/resources/src/components/Pagebar.vue +28 -0
  197. package/template/src/resources/src/components/Refresh.vue +26 -0
  198. package/template/src/resources/src/components/Select.vue +390 -0
  199. package/template/src/resources/src/components/Spinner.vue +42 -0
  200. package/template/src/resources/src/components/Switch.vue +65 -0
  201. package/template/src/resources/src/components/TextArea.vue +121 -0
  202. package/template/src/resources/src/components/Toast.vue +56 -0
  203. package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
  204. package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
  205. package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
  206. package/template/src/resources/src/components/datatable/index.vue +237 -0
  207. package/template/src/resources/src/composables/useAuth.ts +52 -0
  208. package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
  209. package/template/src/resources/src/composables/useDialog.ts +5 -0
  210. package/template/src/resources/src/composables/useGum.ts +3 -0
  211. package/template/src/resources/src/composables/usePulse.ts +5 -0
  212. package/template/src/resources/src/env.d.ts +20 -0
  213. package/template/src/resources/src/helpers/nformatter.ts +10 -0
  214. package/template/src/resources/src/helpers/utils.ts +68 -0
  215. package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
  216. package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
  217. package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
  218. package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
  219. package/template/src/resources/src/layouts/Layout/index.vue +76 -0
  220. package/template/src/resources/src/main.ts +27 -0
  221. package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
  222. package/template/src/resources/src/pages/auth/login.vue +93 -0
  223. package/template/src/resources/src/pages/auth/register.vue +130 -0
  224. package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
  225. package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
  226. package/template/src/resources/src/pages/dashboard/index.vue +76 -0
  227. package/template/src/resources/src/plugins/axios.ts +33 -0
  228. package/template/src/resources/src/plugins/browserDetect.ts +55 -0
  229. package/template/src/resources/src/plugins/dialog.ts +167 -0
  230. package/template/src/resources/src/plugins/gum.ts +343 -0
  231. package/template/src/resources/src/plugins/pulse.ts +141 -0
  232. package/template/src/resources/src/plugins/routeProgress.ts +87 -0
  233. package/template/src/resources/src/router/index.ts +85 -0
  234. package/template/src/resources/src/stores/admin-ui.ts +148 -0
  235. package/template/src/resources/src/stores/auth.ts +151 -0
  236. package/template/src/resources/tsconfig.json +19 -0
  237. package/template/src/resources/vite.config.ts +43 -0
  238. package/template/src/storage/logs/app.log +20179 -0
  239. package/template/src/storage/logs/fatal.log +727 -0
  240. package/template/tsconfig.json +20 -0
@@ -0,0 +1,107 @@
1
+ import { config } from "dotenv";
2
+ import { expand } from "dotenv-expand";
3
+ import { z } from "zod";
4
+
5
+ expand(config({ override: true }));
6
+
7
+ const envSchema = z
8
+ .object({
9
+ APP_NAME: z.string().default("nexgen"),
10
+ APP_ENV: z.enum(["development", "production", "test"]).default("development"),
11
+ APP_PORT: z.coerce.number().default(3000),
12
+ APP_URL: z.string().trim().min(1, "APP_URL is required in .env"),
13
+ FRONTEND_URL: z.string().optional().transform((value) => value?.trim() || undefined),
14
+ OPEN_API: z.enum(["true", "false"]).default("true").transform((value) => value === "true"),
15
+ LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("debug"),
16
+ CORS_ORIGIN: z.string().default("*"),
17
+ DATABASE_URL: z.string().default("sqlite:./src/storage/database/nexgen.sqlite"),
18
+ STORAGE_DRIVER: z.enum(["local", "s3"]).default("local"),
19
+ STORAGE_DISK: z.enum(["public", "private", "tmp"]).default("public"),
20
+ STORAGE_BUCKET: z.string().optional().transform((value) => value?.trim() || undefined),
21
+ STORAGE_REGION: z.string().default("us-east-1"),
22
+ STORAGE_ENDPOINT: z.string().optional().transform((value) => value?.trim() || undefined),
23
+ STORAGE_ACCESS_KEY_ID: z.string().optional().transform((value) => value?.trim() || undefined),
24
+ STORAGE_SECRET_ACCESS_KEY: z.string().optional().transform((value) => value?.trim() || undefined),
25
+ STORAGE_FORCE_PATH_STYLE: z.enum(["true", "false"]).default("false").transform((value) => value === "true"),
26
+ STORAGE_SIGNED_URL_TTL_SECONDS: z.coerce.number().default(900),
27
+ REDIS: z.enum(["true", "false"]).default("false").transform((value) => value === "true"),
28
+ REDIS_URL: z.string().default("redis://127.0.0.1:6379"),
29
+ REDIS_PREFIX: z.string().default("nexgen").transform((value) => value.trim()),
30
+ BULLMQ_UI_ALLOWED_EMAILS: z.string().default(""),
31
+ SESSION_COOKIE: z.string().default("nexgen_session"),
32
+ SESSION_TTL_SECONDS: z.coerce.number().default(7200),
33
+ CACHE_TTL_SECONDS: z.coerce.number().default(3600),
34
+ SOCKET: z.enum(["true", "false"]).default("true").transform((value) => value === "true"),
35
+ FRONTEND: z.enum(["true", "false"]).default("true").transform((value) => value === "true"),
36
+ JWT_ACCESS_SECRET: z.string(),
37
+ JWT_REFRESH_SECRET: z.string(),
38
+ JWT_ACCESS_EXPIRY: z.coerce.number().default(900),
39
+ JWT_REFRESH_EXPIRY: z.coerce.number().default(2592000),
40
+ JWT_REFRESH_REMEMBER_EXPIRY: z.coerce.number().default(604800),
41
+ COOKIE_NAME: z.string().default("nexgen"),
42
+ COOKIE_SECRET: z.string(),
43
+ REDIS_COMMANDER_PORT: z.coerce.number().default(1369),
44
+ MAIL_PORT: z.coerce.number().default(1089),
45
+ MAILDEV_WEB_PORT: z.coerce.number().default(1080),
46
+ MAIL_HOST: z.string().default("127.0.0.1"),
47
+ MAIL_USERNAME: z.string().default(""),
48
+ MAIL_PASSWORD: z.string().default(""),
49
+ MAIL_FROM_ADDRESS: z.string().default("noreply@nexgen.local"),
50
+ MAIL_FAIL_SILENT: z.enum(["true", "false"]).default("true").transform((value) => value === "true"),
51
+ AUTH_REQUIRE_EMAIL_VERIFICATION: z.enum(["true", "false"]).default("false").transform((value) => value === "true"),
52
+ }).superRefine((data, ctx) => {
53
+ if (data.STORAGE_DRIVER === "s3") {
54
+ if (!data.STORAGE_BUCKET) {
55
+ ctx.addIssue({
56
+ code: "custom",
57
+ path: ["STORAGE_BUCKET"],
58
+ message: "STORAGE_BUCKET is required when STORAGE_DRIVER=s3"
59
+ });
60
+ }
61
+
62
+ if (!data.STORAGE_ACCESS_KEY_ID) {
63
+ ctx.addIssue({
64
+ code: "custom",
65
+ path: ["STORAGE_ACCESS_KEY_ID"],
66
+ message: "STORAGE_ACCESS_KEY_ID is required when STORAGE_DRIVER=s3"
67
+ });
68
+ }
69
+
70
+ if (!data.STORAGE_SECRET_ACCESS_KEY) {
71
+ ctx.addIssue({
72
+ code: "custom",
73
+ path: ["STORAGE_SECRET_ACCESS_KEY"],
74
+ message: "STORAGE_SECRET_ACCESS_KEY is required when STORAGE_DRIVER=s3"
75
+ });
76
+ }
77
+ }
78
+
79
+ for (const key of ["JWT_ACCESS_SECRET", "JWT_REFRESH_SECRET", "COOKIE_SECRET"] as const) {
80
+ if (!(data as any)[key]) {
81
+ ctx.addIssue({
82
+ code: "custom",
83
+ path: [key],
84
+ message: `${key} is required`
85
+ });
86
+ }
87
+ }
88
+
89
+ if (!data.REDIS) return;
90
+
91
+ if (!data.REDIS_PREFIX) {
92
+ ctx.addIssue({
93
+ code: "custom",
94
+ path: ["REDIS_PREFIX"],
95
+ message: "REDIS_PREFIX is required when REDIS=true"
96
+ });
97
+ }
98
+ });
99
+
100
+ const parsedEnv = envSchema.parse(process.env);
101
+
102
+ export const env = {
103
+ ...parsedEnv,
104
+ FRONTEND_URL: parsedEnv.FRONTEND_URL
105
+ };
106
+
107
+ export type Env = typeof env;
@@ -0,0 +1,81 @@
1
+ import { env } from "@/env.js";
2
+ import { redisClientIfReady } from "@/framework/redis/client.js";
3
+
4
+ function cacheKey(key: string) {
5
+ return `${env.REDIS_PREFIX}:cache:${key}`;
6
+ }
7
+
8
+ /**
9
+ * Why: Provides Redis-backed cache helpers with graceful fallback behavior.
10
+ * When: Controllers/jobs need temporary computed data caching.
11
+ * Where: Application module code via facade.
12
+ * How: Uses namespaced Redis keys and JSON serialization.
13
+ */
14
+ export const cache = {
15
+ /**
16
+ * Why: Reads cached JSON value by key.
17
+ * When: A feature wants fast lookup before recomputing.
18
+ * Where: Controllers/jobs/services using shared cache.
19
+ * How: Fetches namespaced Redis key and parses JSON.
20
+ */
21
+ async get<T>(key: string, fallback: T | null = null) {
22
+ const client = redisClientIfReady();
23
+ if (!client) return fallback;
24
+
25
+ const value = await client.get(cacheKey(key));
26
+ return value ? (JSON.parse(value) as T) : fallback;
27
+ },
28
+
29
+ /**
30
+ * Why: Stores value in cache with TTL.
31
+ * When: Data should be reused for a limited period.
32
+ * Where: Application code caching expensive results.
33
+ * How: Serializes value and writes with Redis EX seconds.
34
+ */
35
+ async put(key: string, value: unknown, ttl = env.CACHE_TTL_SECONDS) {
36
+ const client = redisClientIfReady();
37
+ if (!client) return false;
38
+
39
+ await client.set(cacheKey(key), JSON.stringify(value), "EX", ttl);
40
+ return true;
41
+ },
42
+
43
+ /**
44
+ * Why: Deletes a cached value by key.
45
+ * When: Cached data becomes stale or must be invalidated.
46
+ * Where: Write/update flows that change source data.
47
+ * How: Removes namespaced Redis cache key.
48
+ */
49
+ async forget(key: string) {
50
+ const client = redisClientIfReady();
51
+ if (!client) return false;
52
+
53
+ await client.del(cacheKey(key));
54
+ return true;
55
+ },
56
+
57
+ /**
58
+ * Why: Returns cached value or computes/stores a fresh one.
59
+ * When: A feature needs cache-aside behavior.
60
+ * Where: Read paths with potentially expensive callbacks.
61
+ * How: Attempts get first, then executes callback and put.
62
+ */
63
+ async remember<T>(key: string, ttl: number, callback: () => Promise<T>) {
64
+ const cached = await cache.get<T>(key);
65
+ if (cached !== null) return cached;
66
+
67
+ const fresh = await callback();
68
+ await this.put(key, fresh, ttl);
69
+ return fresh;
70
+ },
71
+
72
+ /**
73
+ * Why: Indicates whether Redis cache backend is currently usable.
74
+ * When: Runtime checks need to branch on cache availability.
75
+ * Where: Health/status endpoints and conditional flows.
76
+ * How: Confirms REDIS flag and ready Redis client instance.
77
+ */
78
+ isAvailable() {
79
+ return env.REDIS && redisClientIfReady() !== null;
80
+ }
81
+ };
@@ -0,0 +1,168 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { env } from "@/env.js";
4
+ import * as schema from "@/database/schema.js";
5
+
6
+ export type Dialect = "sqlite" | "mysql" | "postgresql";
7
+
8
+ let databaseInstance: any;
9
+ let pool: any;
10
+ let dialect: Dialect;
11
+
12
+ /**
13
+ * Why: Resolves the active DB driver from DATABASE_URL.
14
+ * When: Called during bootstrap and dialect-specific branching.
15
+ * Where: Used by init/close and schema generation helpers.
16
+ * How: Checks DATABASE_URL prefix and maps to sqlite/mysql/postgresql.
17
+ */
18
+ export function detectDialect(): Dialect {
19
+ const url = env.DATABASE_URL.toLowerCase();
20
+ if (url.startsWith("mysql")) return "mysql";
21
+ if (url.startsWith("postgres")) return "postgresql";
22
+ return "sqlite";
23
+ }
24
+
25
+ /**
26
+ * Why: Creates a singleton Drizzle connection for the current dialect.
27
+ * When: Called once at app/worker/scheduler startup.
28
+ * Where: Framework bootstrap entry points.
29
+ * How: Builds dialect-specific pool/client and stores it in module state.
30
+ */
31
+ export async function initDatabase() {
32
+ if (databaseInstance) return databaseInstance;
33
+
34
+ dialect = detectDialect();
35
+
36
+ if (dialect === "sqlite") {
37
+ let drizzleSqlite: any;
38
+ let Database: any;
39
+ try {
40
+ [{ drizzle: drizzleSqlite }, { default: Database }] = await Promise.all([
41
+ import("drizzle-orm/better-sqlite3"),
42
+ import("better-sqlite3")
43
+ ]);
44
+ } catch {
45
+ throw new Error(
46
+ "Missing sqlite dependencies. Install with: bun add drizzle-orm better-sqlite3"
47
+ );
48
+ }
49
+
50
+ const file = path.resolve(process.cwd(), env.DATABASE_URL.replace(/^sqlite:/, ""));
51
+ await fs.mkdir(path.dirname(file), { recursive: true });
52
+ pool = new Database(file);
53
+ pool.pragma("foreign_keys = ON");
54
+ databaseInstance = drizzleSqlite(pool, { schema });
55
+ return databaseInstance;
56
+ }
57
+
58
+ if (dialect === "mysql") {
59
+ let drizzleMysql: any;
60
+ let mysql: any;
61
+ try {
62
+ [{ drizzle: drizzleMysql }, mysql] = await Promise.all([
63
+ import("drizzle-orm/mysql2"),
64
+ import("mysql2/promise")
65
+ ]);
66
+ } catch {
67
+ throw new Error("Missing mysql dependencies. Install with: bun add drizzle-orm mysql2");
68
+ }
69
+
70
+ pool = mysql.createPool({ uri: env.DATABASE_URL, connectionLimit: 10, enableKeepAlive: true });
71
+ databaseInstance = drizzleMysql(pool, { schema, mode: "default" });
72
+ return databaseInstance;
73
+ }
74
+
75
+ let drizzlePg: any;
76
+ let PgPool: any;
77
+ try {
78
+ [{ drizzle: drizzlePg }, { Pool: PgPool }] = await Promise.all([
79
+ import("drizzle-orm/node-postgres"),
80
+ import("pg")
81
+ ]);
82
+ } catch {
83
+ throw new Error("Missing postgres dependencies. Install with: bun add drizzle-orm pg");
84
+ }
85
+
86
+ pool = new PgPool({ connectionString: env.DATABASE_URL });
87
+ databaseInstance = drizzlePg(pool, { schema });
88
+ return databaseInstance;
89
+ }
90
+
91
+ /**
92
+ * Why: Returns initialized Drizzle instance for queries.
93
+ * When: Used by app code after bootstrap.
94
+ * Where: Facade `db` proxy and direct internal consumers.
95
+ * How: Throws if initDatabase has not completed.
96
+ */
97
+ export function database() {
98
+ if (!databaseInstance) throw new Error("Database is not initialized");
99
+ return databaseInstance;
100
+ }
101
+
102
+ /**
103
+ * Why: Exposes low-level pool/client for advanced operations.
104
+ * When: Needed by internals that require native pool behavior.
105
+ * Where: Scheduler lock and infrastructure helpers.
106
+ * How: Returns shared pool and guards against uninitialized access.
107
+ */
108
+ export function databasePool() {
109
+ if (!pool) throw new Error("Database pool is not initialized");
110
+ return pool;
111
+ }
112
+
113
+ /**
114
+ * Why: Gracefully closes DB resources during shutdown/reset flows.
115
+ * When: Server stop, worker stop, migration/reset commands.
116
+ * Where: Runtime lifecycle scripts.
117
+ * How: Detects dialect, closes pool, and clears singleton state.
118
+ */
119
+ export async function closeDatabase() {
120
+ if (!pool) return;
121
+
122
+ const activeDialect = databaseDialect();
123
+
124
+ if (activeDialect === "sqlite") {
125
+ pool.close?.();
126
+ pool = undefined;
127
+ databaseInstance = undefined;
128
+ return;
129
+ }
130
+
131
+ if (activeDialect === "mysql") {
132
+ await pool.end();
133
+ pool = undefined;
134
+ databaseInstance = undefined;
135
+ return;
136
+ }
137
+
138
+ await pool.end();
139
+ pool = undefined;
140
+ databaseInstance = undefined;
141
+ }
142
+
143
+ /**
144
+ * Why: Returns active dialect for conditional SQL/runtime behavior.
145
+ * When: Dialect-specific features are required.
146
+ * Where: Locking, schema, and shutdown helpers.
147
+ * How: Uses initialized dialect or falls back to detection.
148
+ */
149
+ export function databaseDialect() {
150
+ return dialect || detectDialect();
151
+ }
152
+
153
+ /**
154
+ * Why: Provides ergonomic global query surface without calling database().
155
+ * When: Used by modules, seeders, and facade consumers.
156
+ * Where: Exposed via framework facade as `db`.
157
+ * How: Proxy forwards property access to the initialized Drizzle instance.
158
+ */
159
+ export const db = new Proxy(
160
+ {},
161
+ {
162
+ get(_target, property) {
163
+ const instance = database();
164
+ const value = instance[property as keyof typeof instance];
165
+ return typeof value === "function" ? value.bind(instance) : value;
166
+ }
167
+ }
168
+ ) as any;
@@ -0,0 +1,9 @@
1
+ declare module "better-sqlite3" {
2
+ const Database: any;
3
+ export default Database;
4
+ }
5
+
6
+ declare module "pg" {
7
+ export const Client: any;
8
+ export const Pool: any;
9
+ }
@@ -0,0 +1,200 @@
1
+ import { count, getTableColumns, type SQL } from "drizzle-orm";
2
+ import { db } from "@/framework/database/connection.js";
3
+
4
+ type PaginateOptions = {
5
+ page?: number;
6
+ perPage?: number;
7
+ maxPerPage?: number;
8
+ path?: string;
9
+ where?: SQL<unknown>;
10
+ orderBy?: unknown[];
11
+ };
12
+
13
+ type RequestLike = {
14
+ req: {
15
+ query: (key: string) => string | undefined;
16
+ path: string;
17
+ url?: string;
18
+ };
19
+ };
20
+
21
+ type PaginateQueryOptions<T> = {
22
+ page?: number;
23
+ perPage?: number;
24
+ maxPerPage?: number;
25
+ path?: string;
26
+ total: () => Promise<number>;
27
+ data: (limit: number, offset: number) => Promise<T[]>;
28
+ };
29
+
30
+ export type PaginatedResult<T> = {
31
+ current_page: number;
32
+ data: T[];
33
+ first_page_url: string | null;
34
+ from: number | null;
35
+ last_page: number;
36
+ last_page_url: string | null;
37
+ links: Array<{ url: string | null; label: string; page: number | null; active: boolean; }>;
38
+ next_page_url: string | null;
39
+ path: string;
40
+ per_page: number;
41
+ prev_page_url: string | null;
42
+ to: number | null;
43
+ total: number;
44
+ };
45
+
46
+ function toPositiveInt(value: unknown, fallback: number) {
47
+ const parsed = Number(value);
48
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
49
+ return Math.floor(parsed);
50
+ }
51
+
52
+ function pageUrl(path: string, page: number, perPage: number) {
53
+ if (!path) return null;
54
+ const delimiter = path.includes("?") ? "&" : "?";
55
+ return `${path}${delimiter}page=${page}&per_page=${perPage}`;
56
+ }
57
+
58
+ /**
59
+ * Why: Generic paginator for custom total/data callbacks.
60
+ * When: You need pagination outside direct table helpers.
61
+ * Where: Controllers/services composing complex queries.
62
+ * How: Normalizes page params, computes URLs/meta, and executes callbacks.
63
+ */
64
+ export async function paginateQuery<T = any>(
65
+ options: PaginateQueryOptions<T>
66
+ ): Promise<PaginatedResult<T>> {
67
+ const maxPerPage = toPositiveInt(options.maxPerPage, 100);
68
+ const perPage = Math.min(toPositiveInt(options.perPage, 15), maxPerPage);
69
+ const page = toPositiveInt(options.page, 1);
70
+
71
+ const total = Number(await options.total());
72
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
73
+ const currentPage = Math.min(page, lastPage);
74
+ const safeOffset = (currentPage - 1) * perPage;
75
+ const data = await options.data(perPage, safeOffset);
76
+
77
+ const from = total === 0 ? null : safeOffset + 1;
78
+ const to = total === 0 ? null : safeOffset + data.length;
79
+ const path = options.path || "";
80
+ const firstPageUrl = pageUrl(path, 1, perPage);
81
+ const lastPageUrl = pageUrl(path, lastPage, perPage);
82
+ const prevPageUrl = currentPage > 1 ? pageUrl(path, currentPage - 1, perPage) : null;
83
+ const nextPageUrl = currentPage < lastPage ? pageUrl(path, currentPage + 1, perPage) : null;
84
+
85
+ const links: Array<{ url: string | null; label: string; page: number | null; active: boolean; }> =
86
+ [
87
+ {
88
+ url: prevPageUrl,
89
+ label: "&laquo; Previous",
90
+ page: currentPage > 1 ? currentPage - 1 : null,
91
+ active: false
92
+ }
93
+ ];
94
+
95
+ for (let p = 1; p <= lastPage; p += 1) {
96
+ links.push({
97
+ url: pageUrl(path, p, perPage),
98
+ label: String(p),
99
+ page: p,
100
+ active: p === currentPage
101
+ });
102
+ }
103
+
104
+ links.push({
105
+ url: nextPageUrl,
106
+ label: "Next &raquo;",
107
+ page: currentPage < lastPage ? currentPage + 1 : null,
108
+ active: false
109
+ });
110
+
111
+ return {
112
+ current_page: currentPage,
113
+ data,
114
+ first_page_url: firstPageUrl,
115
+ from,
116
+ last_page: lastPage,
117
+ last_page_url: lastPageUrl,
118
+ links,
119
+ next_page_url: nextPageUrl,
120
+ path,
121
+ per_page: perPage,
122
+ prev_page_url: prevPageUrl,
123
+ to,
124
+ total
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Why: Paginates a Drizzle query using request query params.
130
+ * When: Route handlers receive `page/per_page` from client.
131
+ * Where: API controllers returning list resources.
132
+ * How: Builds count subquery and applies limit/offset to source query.
133
+ */
134
+ export async function paginate<T = any>(
135
+ c: RequestLike,
136
+ query: any,
137
+ perPage = 15,
138
+ options: { maxPerPage?: number; path?: string; } = {}
139
+ ): Promise<PaginatedResult<T>> {
140
+ const page = Number(c.req.query("page") || 1);
141
+ const size = c.req.query("size");
142
+ const perPageQuery = c.req.query("per_page");
143
+ const perPageValue = Number(size || perPageQuery || perPage);
144
+ const fallbackPath = c.req.path;
145
+ const resolvedPath = (() => {
146
+ if (options.path) return options.path;
147
+ if (!c.req.url) return fallbackPath;
148
+ try {
149
+ const parsed = new URL(c.req.url);
150
+ return `${parsed.origin}${parsed.pathname}`;
151
+ } catch {
152
+ return fallbackPath;
153
+ }
154
+ })();
155
+
156
+ const queryForCount = query.as("paginate_rows");
157
+
158
+ return paginateQuery<T>({
159
+ page,
160
+ perPage: perPageValue,
161
+ maxPerPage: options.maxPerPage,
162
+ path: resolvedPath,
163
+ total: async () => {
164
+ const totalRow = await db.select({ total: count() }).from(queryForCount);
165
+ return Number(totalRow[0]?.total ?? 0);
166
+ },
167
+ data: (limit, offset) => query.limit(limit).offset(offset)
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Why: Paginates a full table with optional filters/sorting.
173
+ * When: CRUD list endpoints need simple table pagination.
174
+ * Where: Service/controller code for table resources.
175
+ * How: Runs total + paged select with where/order options.
176
+ */
177
+ export async function paginateTable<T = any>(
178
+ db: any,
179
+ table: any,
180
+ options: PaginateOptions = {}
181
+ ): Promise<PaginatedResult<T>> {
182
+ return paginateQuery<T>({
183
+ page: options.page,
184
+ perPage: options.perPage,
185
+ maxPerPage: options.maxPerPage,
186
+ path: options.path,
187
+ total: async () => {
188
+ let totalQuery = db.select({ total: count() }).from(table);
189
+ if (options.where) totalQuery = totalQuery.where(options.where);
190
+ const totalRow = await totalQuery;
191
+ return Number(totalRow[0]?.total ?? 0);
192
+ },
193
+ data: async (limit, offset) => {
194
+ let dataQuery = db.select(getTableColumns(table)).from(table);
195
+ if (options.where) dataQuery = dataQuery.where(options.where);
196
+ if (options.orderBy?.length) dataQuery = dataQuery.orderBy(...options.orderBy);
197
+ return dataQuery.limit(limit).offset(offset);
198
+ }
199
+ });
200
+ }
@@ -0,0 +1,26 @@
1
+ import { detectDialect } from "@/framework/database/connection.js";
2
+
3
+ /**
4
+ * Why: Picks dialect-specific model exports from composite schema modules.
5
+ * When: Building generated schema for active DB dialect.
6
+ * Where: Maker/database schema generation pipeline.
7
+ * How: Filters objects containing sqlite/mysql/postgresql keys and selects current dialect.
8
+ */
9
+ export function normalizeSchemaExports(source: Record<string, any>) {
10
+ const dialect = detectDialect();
11
+ const out: Record<string, any> = {};
12
+
13
+ for (const [key, value] of Object.entries(source)) {
14
+ if (
15
+ value &&
16
+ typeof value === "object" &&
17
+ "sqlite" in value &&
18
+ "mysql" in value &&
19
+ "postgresql" in value
20
+ ) {
21
+ out[key] = value[dialect];
22
+ }
23
+ }
24
+
25
+ return out;
26
+ }
@@ -0,0 +1,33 @@
1
+ import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
2
+ import { closeDatabase, initDatabase } from "@/framework/database/connection.js";
3
+
4
+ /**
5
+ * Why: Runs module seeders for all modules or one targeted module.
6
+ * When: CLI executes `db:seed` flows.
7
+ * Where: Framework database seeding runtime.
8
+ * How: Boots DB, discovers seeder files, executes default exports, then closes DB.
9
+ */
10
+ await initDatabase();
11
+
12
+ try {
13
+ const moduleArg = process.argv.find((arg) => arg.startsWith("--module="));
14
+ const moduleName = moduleArg?.split("=")[1]?.trim();
15
+ const pattern = moduleName
16
+ ? `${moduleName}/database/seeders/*.{ts,js}`
17
+ : "**/database/seeders/*.{ts,js}";
18
+
19
+ const files = await discoverModuleFiles(pattern);
20
+
21
+ for (const file of files) {
22
+ const seeder = await importFile(file);
23
+ if (typeof seeder.default === "function") {
24
+ await seeder.default();
25
+ }
26
+ }
27
+
28
+ console.log(
29
+ `Executed ${files.length} seeder file(s)${moduleName ? ` for module ${moduleName}` : ""}`
30
+ );
31
+ } finally {
32
+ await closeDatabase();
33
+ }
@@ -0,0 +1,57 @@
1
+ import { queueJob } from "@/framework/queue/queue.js";
2
+ import { broadcast, type BroadcastOptions } from "@/framework/realtime/broadcast.js";
3
+
4
+ type CommandHandler = (payload: any) => Promise<any>;
5
+
6
+ const commandHandlers = new Map<string, CommandHandler>();
7
+
8
+ /**
9
+ * Why: Registers in-memory command handler for synchronous dispatch.
10
+ * When: Module startup files register command handlers.
11
+ * Where: Job/command style module code.
12
+ * How: Stores handler in a process-level map by command name.
13
+ */
14
+ export function command(name: string, handler: CommandHandler) {
15
+ commandHandlers.set(name, handler);
16
+ }
17
+
18
+ /**
19
+ * Why: Executes command immediately or queues it for async processing.
20
+ * When: Application logic needs command pattern behavior.
21
+ * Where: Controllers, jobs, and service layers.
22
+ * How: Runs local handler unless async requested; otherwise enqueues.
23
+ */
24
+ export async function dispatchCommand(
25
+ name: string,
26
+ payload: any,
27
+ options: { async?: boolean; queue?: string } = {}
28
+ ) {
29
+ if (!options.async) {
30
+ const handler = commandHandlers.get(name);
31
+ if (handler) return await handler(payload);
32
+ }
33
+
34
+ return await queueJob(name, payload, { queue: options.queue || "default" });
35
+ }
36
+
37
+ /**
38
+ * Why: Dispatches domain events with optional queueing and websocket fan-out.
39
+ * When: Domain actions need side effects or realtime notifications.
40
+ * Where: Controllers/jobs across modules.
41
+ * How: Broadcasts via Socket.IO and optionally enqueues job.
42
+ */
43
+ export async function dispatchEvent(
44
+ name: string,
45
+ payload: any,
46
+ options: { queue?: boolean | string; broadcast?: BroadcastOptions } = {}
47
+ ) {
48
+ if (options.broadcast) {
49
+ broadcast(name, payload, options.broadcast);
50
+ }
51
+
52
+ if (options.queue) {
53
+ return await queueJob(name, payload, {
54
+ queue: typeof options.queue === "string" ? options.queue : "default"
55
+ });
56
+ }
57
+ }