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,213 @@
1
+ import { Job, Queue, QueueEvents, Worker, type JobsOptions } from "bullmq";
2
+ import { env } from "@/env.js";
3
+ import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
4
+ import { redisClientIfReady } from "@/framework/redis/client.js";
5
+
6
+ export type QueueHandler = (job: Job) => Promise<any>;
7
+
8
+ const queues = new Map<string, Queue>();
9
+ const events = new Map<string, QueueEvents>();
10
+ const handlers = new Map<string, QueueHandler>();
11
+ const workers: Worker[] = [];
12
+
13
+ function queuePrefix() {
14
+ return `${env.REDIS_PREFIX}:queue`;
15
+ }
16
+
17
+ function key(queue: string, job: string) {
18
+ return `${queue}:${job}`;
19
+ }
20
+
21
+ /**
22
+ * Why: Registers a job handler for a queue + job name.
23
+ * When: Module job files are loaded during boot.
24
+ * Where: Module job definition files under `src/modules/<module>/jobs`.
25
+ * How: Stores handler in an in-memory lookup key.
26
+ */
27
+ export function shouldQueue(job: string, queue: string, handler: QueueHandler) {
28
+ handlers.set(key(queue || "default", job), handler);
29
+ }
30
+
31
+ /**
32
+ * Why: Gets or lazily creates a BullMQ queue instance.
33
+ * When: Enqueueing jobs or ensuring queue resources.
34
+ * Where: Queue facade and dispatcher.
35
+ * How: Reuses shared Redis connection and memoized queue map.
36
+ */
37
+ export function getQueue(queue = "default") {
38
+ const client = redisClientIfReady();
39
+ if (!client) return null;
40
+
41
+ if (!queues.has(queue)) {
42
+ queues.set(
43
+ queue,
44
+ new Queue(queue, {
45
+ connection: client,
46
+ prefix: queuePrefix(),
47
+ defaultJobOptions: {
48
+ attempts: 3,
49
+ backoff: { type: "exponential", delay: 3000 },
50
+ removeOnComplete: 1000,
51
+ removeOnFail: 5000
52
+ }
53
+ })
54
+ );
55
+ }
56
+
57
+ return queues.get(queue)!;
58
+ }
59
+
60
+ /**
61
+ * Why: Facade-friendly alias for getQueue.
62
+ * When: Consumers want fluent queue access.
63
+ * Where: Framework facade/API usage.
64
+ * How: Delegates directly to getQueue.
65
+ */
66
+ export function queue(queueName = "default") {
67
+ return getQueue(queueName);
68
+ }
69
+
70
+ /**
71
+ * Why: Ensures configured queues exist before UI/runtime usage.
72
+ * When: BullBoard setup or startup warmup.
73
+ * Where: Queue UI/bootstrap code.
74
+ * How: Iterates names and resolves queue instances.
75
+ */
76
+ export function ensureQueues(queueNames: string[]) {
77
+ for (const queueName of queueNames) {
78
+ getQueue(queueName);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Why: Returns all created queue instances.
84
+ * When: Queue introspection and dashboard binding.
85
+ * Where: BullBoard setup code.
86
+ * How: Converts internal queue map to array.
87
+ */
88
+ export function getAllQueues() {
89
+ return Array.from(queues.values());
90
+ }
91
+
92
+ /**
93
+ * Why: Adds a job to BullMQ with project defaults.
94
+ * When: Commands/events need background execution.
95
+ * Where: Dispatcher and feature modules.
96
+ * How: Resolves queue then calls add with retry/backoff options.
97
+ */
98
+ export async function queueJob(
99
+ job: string,
100
+ data: any,
101
+ options: {
102
+ queue?: string;
103
+ delay?: number;
104
+ attempts?: number;
105
+ jobId?: string;
106
+ priority?: number;
107
+ removeOnComplete?: JobsOptions["removeOnComplete"];
108
+ removeOnFail?: JobsOptions["removeOnFail"];
109
+ backoff?: JobsOptions["backoff"];
110
+ } = {}
111
+ ) {
112
+ const queueName = options.queue || "default";
113
+ const queue = getQueue(queueName);
114
+ if (!queue) return null;
115
+
116
+ return await queue.add(job, data, {
117
+ delay: options.delay ? options.delay * 1000 : 0,
118
+ attempts: options.attempts ?? 3,
119
+ jobId: options.jobId,
120
+ priority: options.priority,
121
+ removeOnComplete: options.removeOnComplete ?? 1000,
122
+ removeOnFail: options.removeOnFail ?? 5000,
123
+ backoff: options.backoff ?? { type: "exponential", delay: 3000 }
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Why: Loads module job files so handlers are registered.
129
+ * When: Worker start and explicit queue boot flows.
130
+ * Where: Worker/runtime bootstrap.
131
+ * How: Discovers jobs glob and imports each file.
132
+ */
133
+ export async function bootQueueJobs() {
134
+ const files = await discoverModuleFiles("**/jobs/*.{ts,js}");
135
+ for (const file of files) await importFile(file);
136
+ return files.length;
137
+ }
138
+
139
+ /**
140
+ * Why: Starts BullMQ workers for selected queues.
141
+ * When: Queue worker process boots.
142
+ * Where: Worker runtime entrypoint.
143
+ * How: Boots handlers, creates QueueEvents, and creates workers.
144
+ */
145
+ export async function startQueueWorker(queueNames = ["default"]) {
146
+ await bootQueueJobs();
147
+ const client = redisClientIfReady();
148
+ if (!client) throw new Error("Redis is required for queue workers");
149
+
150
+ for (const queueName of queueNames) {
151
+ if (!events.has(queueName)) {
152
+ events.set(
153
+ queueName,
154
+ new QueueEvents(queueName, { connection: client, prefix: queuePrefix() })
155
+ );
156
+ }
157
+
158
+ const worker = new Worker(
159
+ queueName,
160
+ async (job) => {
161
+ console.log(`[${new Date().toLocaleTimeString()}] Processing: ${job.name} (${queueName})`);
162
+ const handler = handlers.get(key(queueName, job.name));
163
+ if (!handler) throw new Error(`No handler registered for ${queueName}:${job.name}`);
164
+ return await handler(job);
165
+ },
166
+ { connection: client, prefix: queuePrefix(), concurrency: 10 }
167
+ );
168
+
169
+ worker.on("completed", (job) => {
170
+ console.log(`[${new Date().toLocaleTimeString()}] Processed: ${job.name} (${queueName})`);
171
+ });
172
+
173
+ worker.on("failed", (job, error) => {
174
+ const jobName = job?.name ?? "unknown";
175
+ console.log(`[${new Date().toLocaleTimeString()}] Failed: ${jobName} (${queueName}) - ${error.message}`);
176
+ });
177
+
178
+ workers.push(worker);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Why: Clears queue-related Redis keys for reset workflows.
184
+ * When: Local cleanup and maintenance commands.
185
+ * Where: `queue:clear` command.
186
+ * How: Scans prefixed keys and deletes in batches.
187
+ */
188
+ export async function clearQueue() {
189
+ const client = redisClientIfReady();
190
+ if (!client) return;
191
+
192
+ const stream = client.scanStream({ match: `${queuePrefix()}*`, count: 100 });
193
+ for await (const keys of stream) {
194
+ if (keys.length) await client.del(keys);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Why: Gracefully stops all workers/events/queues.
200
+ * When: Process shutdown.
201
+ * Where: Server/worker lifecycle hooks.
202
+ * How: Closes resources with Promise.allSettled and clears registries.
203
+ */
204
+ export async function stopQueueRuntime() {
205
+ await Promise.allSettled(workers.map((worker) => worker.close()));
206
+ workers.length = 0;
207
+
208
+ await Promise.allSettled(Array.from(events.values()).map((queueEvents) => queueEvents.close()));
209
+ events.clear();
210
+
211
+ await Promise.allSettled(Array.from(queues.values()).map((queue) => queue.close()));
212
+ queues.clear();
213
+ }
@@ -0,0 +1,104 @@
1
+ import { createBullBoard } from "@bull-board/api";
2
+ import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
3
+ import { HonoAdapter } from "@bull-board/hono";
4
+ import { serveStatic } from "@hono/node-server/serve-static";
5
+ import { env } from "@/env.js";
6
+ import { redisClientIfReady } from "@/framework/redis/client.js";
7
+ import { ensureQueues, getAllQueues } from "@/framework/queue/queue.js";
8
+ import { parseCsvOrFallback } from "@/framework/support/lifecycle.js";
9
+ import { authMiddleware } from "@/middlewares/auth-middleware.js";
10
+
11
+ function redisAvailable() {
12
+ return redisClientIfReady() !== null;
13
+ }
14
+
15
+ function allowedBullmqEmails() {
16
+ const emails = parseCsvOrFallback(env.BULLMQ_UI_ALLOWED_EMAILS, []);
17
+ return new Set(emails.map((email) => email.toLowerCase()));
18
+ }
19
+
20
+ /**
21
+ * Why: Initializes BullBoard dashboard with auth protection.
22
+ * When: HTTP kernel boots queue UI integration.
23
+ * Where: Kernel startup and `/bullmq` route mounting.
24
+ * How: Builds protected route; returns unavailable page when Redis is down.
25
+ */
26
+ export function setupBullBoard() {
27
+ const basePath = "/bullmq";
28
+ const allowedEmails = allowedBullmqEmails();
29
+
30
+ const protectRoute = (app: any) => {
31
+ const guard = async (c: any, next: any) => {
32
+ const response = await authMiddleware(c, async () => {
33
+ const auth = c.get("auth");
34
+ const email = String(auth?.email || "")
35
+ .trim()
36
+ .toLowerCase();
37
+
38
+ if (allowedEmails.size > 0 && !allowedEmails.has(email)) {
39
+ return c.json({ message: "Forbidden" }, 403);
40
+ }
41
+
42
+ await next();
43
+ });
44
+
45
+ return response;
46
+ };
47
+
48
+ app.use(basePath, guard);
49
+ app.use(`${basePath}/*`, guard);
50
+ };
51
+
52
+ if (!redisAvailable()) {
53
+ const route = (app: any) => {
54
+ protectRoute(app);
55
+ app.get(basePath, (c: any) =>
56
+ c.html(`
57
+ <!doctype html>
58
+ <html>
59
+ <head>
60
+ <title>Queue Dashboard Unavailable</title>
61
+ <style>
62
+ body { font-family: system-ui, sans-serif; padding: 48px; text-align: center; }
63
+ h1 { color: #dc2626; }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <h1>Queue Dashboard Unavailable</h1>
68
+ <p>Redis is not connected. Check REDIS_URL and make sure Redis is running.</p>
69
+ <p>Redis URL: ${env.REDIS_URL}</p>
70
+ </body>
71
+ </html>
72
+ `)
73
+ );
74
+
75
+ app.get(`${basePath}/*`, (c: any) => c.redirect(basePath));
76
+ };
77
+
78
+ return { basePath, enabled: false, route };
79
+ }
80
+
81
+ ensureQueues(["default", "mail"]);
82
+
83
+ const serverAdapter = new HonoAdapter(serveStatic);
84
+ serverAdapter.setBasePath(basePath);
85
+
86
+ createBullBoard({
87
+ queues: getAllQueues().map((queue) => new BullMQAdapter(queue)),
88
+ serverAdapter,
89
+ options: {
90
+ uiConfig: {
91
+ boardTitle: "Queue Dashboard",
92
+ hideRedisDetails: true
93
+ }
94
+ }
95
+ });
96
+
97
+ const pluginRoute = serverAdapter.registerPlugin();
98
+ const route = (app: any) => {
99
+ protectRoute(app);
100
+ app.route(basePath, pluginRoute);
101
+ };
102
+
103
+ return { basePath, enabled: true, route };
104
+ }
@@ -0,0 +1,33 @@
1
+ import { closeRedis, initRedis, redisError, redisReady } from "@/framework/redis/client.js";
2
+ import { startQueueWorker, stopQueueRuntime } from "@/framework/queue/queue.js";
3
+ import { parseCsvOrFallback, registerShutdownSignals, type ShutdownSignal } from "@/framework/support/lifecycle.js";
4
+ import { logger } from "@/framework/support/logger.js";
5
+
6
+ const queuesArg = process.argv.find((arg) => arg.startsWith("--queue="));
7
+ const queueNames = queuesArg?.split("=")[1];
8
+ const queues = parseCsvOrFallback(queueNames, ["default"]);
9
+
10
+ await initRedis();
11
+ if (!redisReady()) {
12
+ logger.error("Queue worker cannot start because Redis is unavailable", {
13
+ error: redisError()
14
+ });
15
+ process.exit(1);
16
+ }
17
+
18
+ await startQueueWorker(queues);
19
+ console.log(`Queue worker started: ${queues.join(", ")}`);
20
+
21
+ let shuttingDown = false;
22
+
23
+ async function shutdown(signal: ShutdownSignal) {
24
+ if (shuttingDown) return;
25
+ shuttingDown = true;
26
+
27
+ logger.info("Queue worker shutdown signal received", { signal });
28
+ await Promise.allSettled([stopQueueRuntime(), closeRedis()]);
29
+
30
+ process.exit(0);
31
+ }
32
+
33
+ registerShutdownSignals(shutdown);
@@ -0,0 +1,27 @@
1
+ import { socketServer } from "@/framework/realtime/index.js";
2
+
3
+ export type BroadcastOptions = {
4
+ users?: Array<string | number>;
5
+ roles?: string[];
6
+ rooms?: string[];
7
+ auth?: boolean;
8
+ all?: boolean;
9
+ };
10
+
11
+ /**
12
+ * Why: Emits realtime events to targeted audiences.
13
+ * When: Domain events need websocket fan-out.
14
+ * Where: Event dispatcher and module jobs.
15
+ * How: Emits to all/auth/users/roles/custom rooms using Socket.IO rooms.
16
+ */
17
+ export function broadcast(event: string, payload: any, options: BroadcastOptions = {}) {
18
+ const io = socketServer();
19
+ if (!io) return;
20
+
21
+ if (options.all) io.emit(event, payload);
22
+ if (options.auth) io.to("auth").emit(event, payload);
23
+
24
+ for (const user of options.users || []) io.to(`user:${user}`).emit(event, payload);
25
+ for (const role of options.roles || []) io.to(`role:${role}`).emit(event, payload);
26
+ for (const room of options.rooms || []) io.to(room).emit(event, payload);
27
+ }
@@ -0,0 +1 @@
1
+ export { initRealtime, socketServer, closeRealtime } from "@/framework/realtime/socket.js";
@@ -0,0 +1,65 @@
1
+ import type { Socket } from "socket.io";
2
+ import { env } from "@/env.js";
3
+ import { jwt } from "@/framework/support/jwt.js";
4
+ import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
5
+
6
+ export function unauthenticatedRealtimeAuth(): RealtimeAuthContext {
7
+ return { isAuthenticated: false, userId: null, roles: [], payload: null };
8
+ }
9
+
10
+ function parseCookieHeader(header: string | undefined) {
11
+ if (!header) return {} as Record<string, string>;
12
+
13
+ const cookies: Record<string, string> = {};
14
+ for (const part of header.split(";")) {
15
+ const [rawName, ...rawValue] = part.trim().split("=");
16
+ if (!rawName || rawValue.length === 0) continue;
17
+ cookies[rawName] = decodeURIComponent(rawValue.join("="));
18
+ }
19
+
20
+ return cookies;
21
+ }
22
+
23
+ function userIdFromPayload(payload: Record<string, unknown>) {
24
+ const candidate = payload.userId ?? payload.id ?? payload.sub;
25
+ if (candidate === null || candidate === undefined) return null;
26
+ return String(candidate);
27
+ }
28
+
29
+ function rolesFromPayload(payload: Record<string, unknown>) {
30
+ const direct = payload.roles;
31
+ if (Array.isArray(direct)) return direct.map((role) => String(role));
32
+
33
+ const single = payload.role;
34
+ if (single === null || single === undefined) return [];
35
+ return [String(single)];
36
+ }
37
+
38
+ /**
39
+ * Why: Resolves auth context for websocket connections from cookies.
40
+ * When: Socket.IO handshake middleware executes.
41
+ * Where: Realtime server auth pipeline.
42
+ * How: Parses access cookie, verifies JWT, then normalizes user/role claims.
43
+ */
44
+ export async function authFromSocketHandshake(socket: Socket): Promise<RealtimeAuthContext> {
45
+ const cookies = parseCookieHeader(socket.handshake.headers.cookie);
46
+ const token = cookies[`${env.COOKIE_NAME}_access`];
47
+
48
+ if (!token) {
49
+ return unauthenticatedRealtimeAuth();
50
+ }
51
+
52
+ const payload = await jwt.verifyToken(token, "access");
53
+ if (!payload) {
54
+ return unauthenticatedRealtimeAuth();
55
+ }
56
+
57
+ const normalizedPayload = payload as Record<string, unknown>;
58
+
59
+ return {
60
+ isAuthenticated: true,
61
+ userId: userIdFromPayload(normalizedPayload),
62
+ roles: rolesFromPayload(normalizedPayload),
63
+ payload: normalizedPayload
64
+ };
65
+ }
@@ -0,0 +1,132 @@
1
+ import { Server } from "socket.io";
2
+ import { createAdapter } from "@socket.io/redis-adapter";
3
+ import { env } from "@/env.js";
4
+ import { logger } from "@/framework/support/logger.js";
5
+ import {
6
+ authFromSocketHandshake,
7
+ unauthenticatedRealtimeAuth
8
+ } from "@/framework/realtime/socket-cookie.js";
9
+ import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
10
+ import { redisClientIfReady } from "@/framework/redis/client.js";
11
+
12
+ let io: Server | null = null;
13
+ let adapterSubClient: ReturnType<typeof redisClientIfReady> | null = null;
14
+
15
+ function socketAllowedOrigins() {
16
+ const origins = [env.APP_URL, env.FRONTEND_URL].filter(Boolean) as string[];
17
+ if (env.SOCKET) origins.push("https://admin.socket.io");
18
+ return [...new Set(origins)];
19
+ }
20
+
21
+ /**
22
+ * Why: Boots singleton Socket.IO server with auth-aware room assignment.
23
+ * When: HTTP server starts and realtime is enabled.
24
+ * Where: Framework server lifecycle.
25
+ * How: Configures CORS, resolves auth from cookies, and joins role/user rooms.
26
+ */
27
+ export async function initRealtime(server: any) {
28
+ if (!env.SOCKET) {
29
+ logger.info("Socket.IO disabled (SOCKET=false)");
30
+ return null;
31
+ }
32
+
33
+ if (io) return io;
34
+
35
+ io = new Server(server, {
36
+ cors: {
37
+ origin: socketAllowedOrigins(),
38
+ credentials: true
39
+ }
40
+ });
41
+
42
+ if (env.REDIS) {
43
+ const pubClient = redisClientIfReady();
44
+ if (pubClient) {
45
+ adapterSubClient = pubClient.duplicate();
46
+ await adapterSubClient.connect();
47
+ io.adapter(createAdapter(pubClient, adapterSubClient));
48
+ }
49
+ }
50
+
51
+ io.use(async (socket, next) => {
52
+ const auth = await authFromSocketHandshake(socket);
53
+ socket.data.auth = auth as RealtimeAuthContext;
54
+
55
+ if (auth.isAuthenticated) {
56
+ socket.join("auth");
57
+ if (auth.userId) socket.join(`user:${auth.userId}`);
58
+ for (const role of auth.roles) socket.join(`role:${role}`);
59
+ } else {
60
+ socket.join("guest");
61
+ }
62
+
63
+ next();
64
+ });
65
+
66
+ io.on("connection", (socket) => {
67
+ const auth = (socket.data.auth || unauthenticatedRealtimeAuth()) as RealtimeAuthContext;
68
+
69
+ logger.debug("Socket connected", {
70
+ socketId: socket.id,
71
+ authenticated: auth.isAuthenticated,
72
+ userId: auth.userId
73
+ });
74
+
75
+ socket.on("join", (room) => socket.join(String(room)));
76
+ socket.on("disconnect", (reason) => {
77
+ logger.info("Socket disconnected", {
78
+ socketId: socket.id,
79
+ reason,
80
+ authenticated: auth.isAuthenticated,
81
+ userId: auth.userId
82
+ });
83
+ });
84
+ });
85
+
86
+ return io;
87
+ }
88
+
89
+ /**
90
+ * Why: Returns active Socket.IO server instance.
91
+ * When: Broadcast helpers need emitter access.
92
+ * Where: Realtime broadcast/event internals.
93
+ * How: Returns nullable singleton created by initRealtime.
94
+ */
95
+ export function socketServer() {
96
+ return io;
97
+ }
98
+
99
+ /**
100
+ * Why: Alias for socketServer for facade ergonomics.
101
+ * When: Existing consumers use `io` naming.
102
+ * Where: Framework facade export compatibility.
103
+ * How: Returns same singleton Socket.IO instance.
104
+ */
105
+ export function ioServer() {
106
+ return io;
107
+ }
108
+
109
+ /**
110
+ * Why: Closes Socket.IO server gracefully on shutdown.
111
+ * When: Server stop hooks.
112
+ * Where: Framework runtime teardown.
113
+ * How: Awaits close callback and clears singleton reference.
114
+ */
115
+ export async function closeRealtime() {
116
+ if (!io) return;
117
+
118
+ await new Promise<void>((resolve) => {
119
+ io?.close(() => resolve());
120
+ });
121
+
122
+ if (adapterSubClient) {
123
+ try {
124
+ await adapterSubClient.quit();
125
+ } catch {
126
+ adapterSubClient.disconnect();
127
+ }
128
+ adapterSubClient = null;
129
+ }
130
+
131
+ io = null;
132
+ }
@@ -0,0 +1,6 @@
1
+ export type RealtimeAuthContext = {
2
+ isAuthenticated: boolean;
3
+ userId: string | null;
4
+ roles: string[];
5
+ payload: Record<string, unknown> | null;
6
+ };
@@ -0,0 +1,16 @@
1
+ import { instrument } from "@socket.io/admin-ui";
2
+ import type { Server as SocketIOServer } from "socket.io";
3
+ import { env } from "@/env.js";
4
+
5
+ export function setupSocketAdminUI(io: SocketIOServer | null) {
6
+ if (!io || !env.SOCKET || env.APP_ENV === "production") {
7
+ return { enabled: false };
8
+ }
9
+
10
+ instrument(io, {
11
+ auth: false,
12
+ mode: "development"
13
+ });
14
+
15
+ return { enabled: true };
16
+ }