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,126 @@
1
+ import { Redis } from "ioredis";
2
+ import { env } from "@/env.js";
3
+ import { logger } from "@/framework/support/logger.js";
4
+
5
+ export type redis = Redis;
6
+
7
+ let client: redis | null = null;
8
+ let ready = false;
9
+ let lastError: string | null = null;
10
+
11
+ function safeRedisUrl() {
12
+ try {
13
+ const url = new URL(env.REDIS_URL);
14
+ if (url.password) url.password = "***";
15
+ return url.toString();
16
+ } catch {
17
+ return env.REDIS_URL;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Why: Initializes shared Redis connection for cache/queue/events/session.
23
+ * When: Runtime bootstrap before Redis-backed features are used.
24
+ * Where: Server, worker, scheduler startup paths.
25
+ * How: Connects once, tracks readiness, and degrades gracefully if unavailable.
26
+ */
27
+ export async function initRedis() {
28
+ if (client) return client;
29
+
30
+ if (!env.REDIS) {
31
+ ready = false;
32
+ lastError = "REDIS=false";
33
+ logger.info("Redis disabled; cache, session storage, queue, events, and BullBoard unavailable");
34
+ return null;
35
+ }
36
+
37
+ try {
38
+ client = new Redis(env.REDIS_URL, {
39
+ lazyConnect: true,
40
+ enableReadyCheck: true,
41
+ maxRetriesPerRequest: 0,
42
+ retryStrategy: () => null
43
+ });
44
+
45
+ client.on("error", (error) => {
46
+ lastError = error.message;
47
+ if (ready) {
48
+ logger.warn("Redis connection error", {
49
+ redisUrl: safeRedisUrl(),
50
+ error: error.message
51
+ });
52
+ }
53
+ });
54
+
55
+ await client.connect();
56
+ await client.ping();
57
+ ready = true;
58
+ return client;
59
+ } catch (error) {
60
+ lastError ||= error instanceof Error ? error.message : String(error);
61
+ client?.disconnect();
62
+ client = null;
63
+ ready = false;
64
+ logger.warn(`Redis unavailable; continuing without Redis features (${lastError})`);
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Why: Returns the current Redis client instance.
71
+ * When: Internal helpers need direct Redis operations.
72
+ * Where: Cache/session/queue/events modules.
73
+ * How: Returns nullable singleton without creating a new connection.
74
+ */
75
+ export function redis() {
76
+ return client;
77
+ }
78
+
79
+ /**
80
+ * Why: Indicates whether Redis is connected and usable.
81
+ * When: Features need to branch on Redis availability.
82
+ * Where: Cache/session/queue/event internals.
83
+ * How: Combines readiness flag with client presence.
84
+ */
85
+ export function redisReady() {
86
+ return ready && client !== null;
87
+ }
88
+
89
+ /**
90
+ * Why: Provides a single ready-checked Redis client accessor.
91
+ * When: Redis-backed modules need quick availability guard.
92
+ * Where: Cache/session/queue/events/BullBoard internals.
93
+ * How: Returns connected client or null when Redis is unavailable.
94
+ */
95
+ export function redisClientIfReady() {
96
+ return redisReady() ? client : null;
97
+ }
98
+
99
+ /**
100
+ * Why: Exposes last Redis error for diagnostics.
101
+ * When: Startup or health logs need failure detail.
102
+ * Where: Runtime status output.
103
+ * How: Returns cached error message string.
104
+ */
105
+ export function redisError() {
106
+ return lastError;
107
+ }
108
+
109
+ /**
110
+ * Why: Closes Redis connection during graceful shutdown.
111
+ * When: Process stop hooks for server/worker/scheduler.
112
+ * Where: Runtime lifecycle files.
113
+ * How: Attempts quit, falls back to disconnect, then resets state.
114
+ */
115
+ export async function closeRedis() {
116
+ if (!client) return;
117
+
118
+ try {
119
+ await client.quit();
120
+ } catch {
121
+ client.disconnect();
122
+ } finally {
123
+ client = null;
124
+ ready = false;
125
+ }
126
+ }
@@ -0,0 +1,124 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { env } from "@/env.js";
3
+ import { databaseDialect, databasePool } from "@/framework/database/connection.js";
4
+ import { redis, redisReady } from "@/framework/redis/client.js";
5
+
6
+ type LockOptions = { ttlMs?: number; tableName?: string };
7
+
8
+ function table(name?: string) {
9
+ return name || "scheduler_locks";
10
+ }
11
+
12
+ async function ensureTable(tableName?: string) {
13
+ const pool = databasePool();
14
+ const dialect = databaseDialect();
15
+ const lockTable = table(tableName);
16
+
17
+ if (dialect === "mysql") {
18
+ await pool.query(
19
+ `CREATE TABLE IF NOT EXISTS \`${lockTable}\` (name VARCHAR(191) PRIMARY KEY, owner VARCHAR(191) NOT NULL, expires_at BIGINT NOT NULL)`
20
+ );
21
+ return;
22
+ }
23
+
24
+ if (dialect === "postgresql") {
25
+ await pool.query(
26
+ `CREATE TABLE IF NOT EXISTS "${lockTable}" (name VARCHAR(191) PRIMARY KEY, owner VARCHAR(191) NOT NULL, expires_at BIGINT NOT NULL)`
27
+ );
28
+ return;
29
+ }
30
+
31
+ pool.exec(
32
+ `CREATE TABLE IF NOT EXISTS "${lockTable}" (name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at INTEGER NOT NULL)`
33
+ );
34
+ }
35
+
36
+ async function acquireDbLock(name: string, owner: string, ttlMs: number, tableName?: string) {
37
+ await ensureTable(tableName);
38
+ const pool = databasePool();
39
+ const dialect = databaseDialect();
40
+ const lockTable = table(tableName);
41
+ const now = Date.now();
42
+ const expiresAt = now + ttlMs;
43
+
44
+ if (dialect === "postgresql") {
45
+ const result = await pool.query(
46
+ `INSERT INTO "${lockTable}" (name, owner, expires_at) VALUES ($1, $2, $3) ON CONFLICT (name) DO UPDATE SET owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at WHERE "${lockTable}".expires_at < $4 RETURNING owner`,
47
+ [name, owner, expiresAt, now]
48
+ );
49
+ return result.rows?.[0]?.owner === owner;
50
+ }
51
+
52
+ if (dialect === "mysql") {
53
+ await pool.query(
54
+ `INSERT INTO \`${lockTable}\` (name, owner, expires_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE owner = IF(expires_at < ?, VALUES(owner), owner), expires_at = IF(expires_at < ?, VALUES(expires_at), expires_at)`,
55
+ [name, owner, expiresAt, now, now]
56
+ );
57
+ const [rows] = await pool.query(`SELECT owner FROM \`${lockTable}\` WHERE name = ?`, [name]);
58
+ return rows?.[0]?.owner === owner;
59
+ }
60
+
61
+ pool
62
+ .prepare(
63
+ `INSERT INTO "${lockTable}" (name, owner, expires_at) VALUES (?, ?, ?) ON CONFLICT(name) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at WHERE "${lockTable}".expires_at < ?`
64
+ )
65
+ .run(name, owner, expiresAt, now);
66
+ return pool.prepare(`SELECT owner FROM "${lockTable}" WHERE name = ?`).get(name)?.owner === owner;
67
+ }
68
+
69
+ async function releaseDbLock(name: string, owner: string, tableName?: string) {
70
+ const pool = databasePool();
71
+ const dialect = databaseDialect();
72
+ const lockTable = table(tableName);
73
+
74
+ if (dialect === "postgresql")
75
+ return await pool.query(`DELETE FROM "${lockTable}" WHERE name = $1 AND owner = $2`, [
76
+ name,
77
+ owner
78
+ ]);
79
+ if (dialect === "mysql")
80
+ return await pool.query(`DELETE FROM \`${lockTable}\` WHERE name = ? AND owner = ?`, [
81
+ name,
82
+ owner
83
+ ]);
84
+ return pool.prepare(`DELETE FROM "${lockTable}" WHERE name = ? AND owner = ?`).run(name, owner);
85
+ }
86
+
87
+ /**
88
+ * Why: Runs a handler with distributed lock to prevent duplicate execution.
89
+ * When: Scheduled tasks may run across multiple processes/instances.
90
+ * Where: Scheduler runtime around each cron job.
91
+ * How: Uses Redis NX lock when available, otherwise falls back to DB lock table.
92
+ */
93
+ export async function runWithLock<T>(
94
+ name: string,
95
+ handler: () => Promise<T>,
96
+ options: LockOptions = {}
97
+ ) {
98
+ const owner = randomUUID();
99
+ const ttlMs = options.ttlMs || 60_000;
100
+ const client = redis();
101
+
102
+ if (redisReady() && client) {
103
+ const key = `${env.REDIS_PREFIX}:lock:${name}`;
104
+ const ok = await client.set(key, owner, "PX", ttlMs, "NX");
105
+ if (!ok) return { ran: false, backend: "redis" as const };
106
+
107
+ try {
108
+ const result = await handler();
109
+ return { ran: true, backend: "redis" as const, result };
110
+ } finally {
111
+ await client.del(key);
112
+ }
113
+ }
114
+
115
+ const ok = await acquireDbLock(name, owner, ttlMs, options.tableName);
116
+ if (!ok) return { ran: false, backend: "db" as const };
117
+
118
+ try {
119
+ const result = await handler();
120
+ return { ran: true, backend: "db" as const, result };
121
+ } finally {
122
+ await releaseDbLock(name, owner, options.tableName);
123
+ }
124
+ }
@@ -0,0 +1,26 @@
1
+ import { initDatabase } from "@/framework/database/connection.js";
2
+ import { stopQueueRuntime, bootQueueJobs } from "@/framework/queue/queue.js";
3
+ import { closeRedis, initRedis } from "@/framework/redis/client.js";
4
+ import { startScheduler, stopScheduler } from "@/framework/scheduler/scheduler.js";
5
+ import { registerShutdownSignals, type ShutdownSignal } from "@/framework/support/lifecycle.js";
6
+ import { logger } from "@/framework/support/logger.js";
7
+
8
+ await initDatabase();
9
+ await initRedis();
10
+ await bootQueueJobs();
11
+ await startScheduler();
12
+ console.log("Scheduler started");
13
+
14
+ let shuttingDown = false;
15
+
16
+ async function shutdown(signal: ShutdownSignal) {
17
+ if (shuttingDown) return;
18
+ shuttingDown = true;
19
+
20
+ logger.info("Scheduler shutdown signal received", { signal });
21
+ await Promise.allSettled([Promise.resolve(stopScheduler()), stopQueueRuntime(), closeRedis()]);
22
+
23
+ process.exit(0);
24
+ }
25
+
26
+ registerShutdownSignals(shutdown);
@@ -0,0 +1,82 @@
1
+ import cron from "node-cron";
2
+ import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
3
+ import { runWithLock } from "@/framework/scheduler/lock.js";
4
+
5
+ type Schedule = {
6
+ name: string;
7
+ expression: string;
8
+ handler: () => void | Promise<void>;
9
+ timezone?: string;
10
+ runOnInit?: boolean;
11
+ enabled?: boolean;
12
+ ttlMs?: number;
13
+ };
14
+
15
+ const schedules: Schedule[] = [];
16
+ const tasks: Array<{ stop: () => void; destroy?: () => void }> = [];
17
+
18
+ /**
19
+ * Why: Registers a cron schedule definition at runtime.
20
+ * When: Module console/schedule files are imported.
21
+ * Where: Scheduler-related module files.
22
+ * How: Pushes normalized schedule config into in-memory registry.
23
+ */
24
+ export function defineSchedule(schedule: Schedule) {
25
+ schedules.push({ enabled: true, runOnInit: false, ttlMs: 120_000, ...schedule });
26
+ }
27
+
28
+ /**
29
+ * Why: Loads module schedule files so jobs are registered.
30
+ * When: Scheduler boot process starts.
31
+ * Where: Scheduler worker startup.
32
+ * How: Discovers schedules/console files and imports them.
33
+ */
34
+ export async function bootSchedules() {
35
+ const files = await discoverModuleFiles("**/{schedules,console}/*.{ts,js}");
36
+ for (const file of files) await importFile(file);
37
+ return files.length;
38
+ }
39
+
40
+ /**
41
+ * Why: Starts cron tasks for all enabled schedules.
42
+ * When: Scheduler worker process boots.
43
+ * Where: `schedule:work` runtime.
44
+ * How: Wraps each handler with distributed lock and schedules via node-cron.
45
+ */
46
+ export async function startScheduler() {
47
+ await bootSchedules();
48
+
49
+ for (const schedule of schedules) {
50
+ if (!schedule.enabled) continue;
51
+
52
+ const run = () =>
53
+ runWithLock(
54
+ schedule.name,
55
+ async () => {
56
+ await schedule.handler();
57
+ },
58
+ { ttlMs: schedule.ttlMs }
59
+ );
60
+
61
+ const task = cron.schedule(schedule.expression, run, { timezone: schedule.timezone });
62
+ tasks.push(task);
63
+ if (schedule.runOnInit) await run();
64
+ }
65
+
66
+ return schedules.length;
67
+ }
68
+
69
+ /**
70
+ * Why: Stops and destroys active cron tasks.
71
+ * When: Scheduler shutdown.
72
+ * Where: Process lifecycle handlers.
73
+ * How: Iterates task registry and invokes stop/destroy.
74
+ */
75
+ export function stopScheduler() {
76
+ for (const task of tasks) {
77
+ task.stop();
78
+ task.destroy?.();
79
+ }
80
+
81
+ tasks.length = 0;
82
+ }
@@ -0,0 +1,147 @@
1
+ import { serve } from "@hono/node-server";
2
+ import type { AddressInfo } from "node:net";
3
+ import { createServer } from "node:http";
4
+ import chalk from "chalk";
5
+ import { env } from "@/env.js";
6
+ import { createKernel } from "@/framework/kernel.js";
7
+ import { stopQueueRuntime } from "@/framework/queue/queue.js";
8
+ import { closeRealtime, initRealtime } from "@/framework/realtime/index.js";
9
+ import { setupSocketAdminUI } from "@/framework/realtime/ui.js";
10
+ import { closeRedis, redisError, redisReady } from "@/framework/redis/client.js";
11
+ import { parseCsvOrFallback, registerShutdownSignals, type ShutdownSignal } from "@/framework/support/lifecycle.js";
12
+ import { logger } from "@/framework/support/logger.js";
13
+
14
+ const redisBackedServices = "cache, session, queue, events, BullBoard";
15
+
16
+ /**
17
+ * Why: Builds reliable local URL string for runtime service output.
18
+ * When: Printing API/docs/BullBoard endpoints at startup.
19
+ * Where: Server runtime logging.
20
+ * How: Derives bound port from server address and normalizes hostname.
21
+ */
22
+ function serverUrl(server: { address(): string | AddressInfo | null; }, pathname = "") {
23
+ const address = server.address();
24
+ const port = typeof address === "object" && address ? address.port : env.APP_PORT;
25
+
26
+ try {
27
+ const currentUrl = new URL(env.APP_URL);
28
+ currentUrl.port = String(port);
29
+ if (currentUrl.hostname === "0.0.0.0" || currentUrl.hostname === "::") {
30
+ currentUrl.hostname = "localhost";
31
+ }
32
+
33
+ return `${currentUrl.toString().replace(/\/$/, "")}${pathname}`;
34
+ } catch {
35
+ return `http://localhost:${port}${pathname}`;
36
+ }
37
+ }
38
+
39
+ const { app, bullBoard } = await createKernel();
40
+
41
+ /**
42
+ * Why: Parses optional dev sidecar views requested by dev command.
43
+ * When: Server starts and prints local tooling URLs.
44
+ * Where: Runtime startup output logic.
45
+ * How: Reads `NEXGEN_DEV_VIEWS` comma-separated env and returns a set.
46
+ */
47
+ function devViews() {
48
+ return new Set(parseCsvOrFallback(process.env.NEXGEN_DEV_VIEWS, []).map((view) => view.toLowerCase()));
49
+ }
50
+
51
+ const server = serve({
52
+ fetch: app.fetch,
53
+ port: env.APP_PORT,
54
+ createServer
55
+ });
56
+
57
+ let shuttingDown = false;
58
+
59
+ /**
60
+ * Why: Gracefully closes HTTP server listener.
61
+ * When: Shutdown signal handling.
62
+ * Where: Server runtime teardown.
63
+ * How: Wraps callback-based close in a promise.
64
+ */
65
+ async function closeHttpServer() {
66
+ await new Promise<void>((resolve) => {
67
+ server.close(() => resolve());
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Why: Coordinates full process shutdown for API runtime.
73
+ * When: SIGINT/SIGTERM received.
74
+ * Where: Server entrypoint lifecycle.
75
+ * How: Stops realtime/events/queue/redis/http resources then exits.
76
+ */
77
+ async function shutdown(signal: ShutdownSignal) {
78
+ if (shuttingDown) return;
79
+ shuttingDown = true;
80
+
81
+ logger.info("Shutdown signal received", { signal });
82
+ await Promise.allSettled([
83
+ closeRealtime(),
84
+ stopQueueRuntime(),
85
+ closeRedis(),
86
+ closeHttpServer()
87
+ ]);
88
+
89
+ process.exit(0);
90
+ }
91
+
92
+ registerShutdownSignals(shutdown);
93
+
94
+ const io = await initRealtime(server);
95
+ const socketAdmin = setupSocketAdminUI(io);
96
+ const views = devViews();
97
+ let redisWarnColor: ((text: string) => string) | null = null;
98
+ console.log(`API Docs: ${serverUrl(server, "/api-docs")}`);
99
+
100
+ if (views.has("studio")) {
101
+ console.log("Drizzle Studio requested: https://local.drizzle.studio (see dev process status)");
102
+ }
103
+
104
+ if (!env.REDIS) {
105
+ redisWarnColor = chalk.cyan;
106
+ console.log(chalk.cyan("Redis disabled (REDIS=false)"));
107
+ console.log(chalk.cyan(`Redis-backed services disabled: ${redisBackedServices}`));
108
+ } else if (redisReady()) {
109
+ console.log(chalk.green(`Redis connected: ${env.REDIS_URL}`));
110
+ console.log(chalk.green(`Redis-backed services enabled: ${redisBackedServices}`));
111
+ } else {
112
+ redisWarnColor = chalk.yellow;
113
+ console.log(chalk.yellow(`Redis unavailable: ${redisError() || "not connected"}`));
114
+ console.log(chalk.yellow(`Redis-backed services unavailable: ${redisBackedServices}`));
115
+ }
116
+
117
+ const bullboardLine = `${bullBoard.enabled ? "BullBoard UI enabled" : "BullBoard UI unavailable"}: ${serverUrl(server, bullBoard.basePath)}`;
118
+ console.log(redisWarnColor ? redisWarnColor(bullboardLine) : bullboardLine);
119
+
120
+ const socketLine = env.SOCKET
121
+ ? `Socket.IO enabled: ${env.APP_URL}${socketAdmin.enabled ? " | Admin UI: https://admin.socket.io" : ""}`
122
+ : "Socket.IO disabled";
123
+ console.log(socketLine);
124
+
125
+ if (views.has("maildev")) {
126
+ const maildevLine = `MailDev: http://localhost:${env.MAILDEV_WEB_PORT} (SMTP ${env.MAIL_PORT})`;
127
+ const line = `${maildevLine} (requested; see dev process status)`;
128
+ console.log(redisWarnColor ? redisWarnColor(line) : line);
129
+ }
130
+
131
+ if (views.has("redis")) {
132
+ const redisUiLine = `Redis UI: http://localhost:${env.REDIS_COMMANDER_PORT}`;
133
+ const line = `${redisUiLine} (requested; see dev process status)`;
134
+ console.log(redisWarnColor ? redisWarnColor(line) : line);
135
+ }
136
+
137
+ if (env.FRONTEND) {
138
+ if (process.env.NEXGEN_FRONTEND_URL) {
139
+ console.log("Frontend UI: " + process.env.NEXGEN_FRONTEND_URL);
140
+ } else {
141
+ console.log("Frontend enabled");
142
+ }
143
+ } else {
144
+ console.log("Frontend disabled (FRONTEND=false)");
145
+ }
146
+
147
+ console.log(`${env.APP_NAME} API running on ${serverUrl(server)}`);
@@ -0,0 +1,116 @@
1
+ import type { Context, Next } from "hono";
2
+ import { getCookie, setCookie } from "hono/cookie";
3
+ import { randomUUID } from "node:crypto";
4
+ import { env } from "@/env.js";
5
+ import { redisClientIfReady } from "@/framework/redis/client.js";
6
+
7
+ /** Extract origin from a URL string. Returns empty string on invalid/missing input. */
8
+ function originOf(url?: string) {
9
+ if (!url) return "";
10
+ try {
11
+ return new URL(url).origin;
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ /** Returns true when FRONTEND_URL and APP_URL point to different origins — requires SameSite=None; Secure for session cookies. */
18
+ function needsCrossSiteCookie() {
19
+ const appOrigin = originOf(env.APP_URL);
20
+ const frontendOrigin = originOf(env.FRONTEND_URL);
21
+ return Boolean(appOrigin && frontendOrigin && appOrigin !== frontendOrigin);
22
+ }
23
+
24
+ /** Builds the Redis key for a session id using the configured prefix. */
25
+ function sessionKey(id: string) {
26
+ return `${env.REDIS_PREFIX}:session:${id}`;
27
+ }
28
+
29
+ /**
30
+ * Global middleware that ensures every request has a session cookie.
31
+ * - Reads existing session cookie from request.
32
+ * - If missing: generates a UUID, sets a new cookie (SameSite=None; Secure if cross-origin, else Lax).
33
+ * - Stores sessionId in Hono context (`c.get("sessionId")`).
34
+ * - Refreshes the Redis session TTL on every request.
35
+ */
36
+ export async function sessionMiddleware(c: Context, next: Next) {
37
+ let sessionId = getCookie(c, env.SESSION_COOKIE);
38
+
39
+ if (!sessionId) {
40
+ sessionId = randomUUID();
41
+ const crossSiteCookie = needsCrossSiteCookie();
42
+ setCookie(c, env.SESSION_COOKIE, sessionId, {
43
+ httpOnly: true,
44
+ sameSite: crossSiteCookie ? "None" : "Lax",
45
+ secure: crossSiteCookie,
46
+ path: "/",
47
+ maxAge: env.SESSION_TTL_SECONDS
48
+ });
49
+ }
50
+
51
+ c.set("sessionId", sessionId);
52
+ await session.refresh(sessionId);
53
+ await next();
54
+ }
55
+
56
+ /** Redis-backed server-side session store. Each session is a namespaced JSON document with configurable TTL. */
57
+ export const session = {
58
+ /** Create a new session document in Redis. Returns the generated session id, or empty string when Redis is unavailable. */
59
+ async start(data: Record<string, unknown> = {}) {
60
+ const client = redisClientIfReady();
61
+ if (!client) return "";
62
+
63
+ const id = randomUUID();
64
+ await client.set(sessionKey(id), JSON.stringify(data), "EX", env.SESSION_TTL_SECONDS);
65
+ return id;
66
+ },
67
+
68
+ /** Get the full session payload for a given id. Returns null when session doesn't exist or Redis is unavailable. */
69
+ async all<T = Record<string, unknown>>(id: string): Promise<T | null> {
70
+ const client = redisClientIfReady();
71
+ if (!client) return null;
72
+
73
+ const data = await client.get(sessionKey(id));
74
+ return data ? JSON.parse(data) : null;
75
+ },
76
+
77
+ /** Read a single key from the session payload. Returns null if the key or session doesn't exist. */
78
+ async get<T>(id: string, key: string): Promise<T | null> {
79
+ const data = await session.all<Record<string, T>>(id);
80
+ return data?.[key] ?? null;
81
+ },
82
+
83
+ /** Write or update a single key in the session payload. Rewrites the entire document with a fresh TTL. Returns false when Redis is unavailable. */
84
+ async put(id: string, key: string, value: unknown) {
85
+ const client = redisClientIfReady();
86
+ if (!client) return false;
87
+
88
+ const data = (await session.all<Record<string, unknown>>(id)) || {};
89
+ data[key] = value;
90
+ await client.set(sessionKey(id), JSON.stringify(data), "EX", env.SESSION_TTL_SECONDS);
91
+ return true;
92
+ },
93
+
94
+ /** Extend the TTL of an existing session. Called automatically by sessionMiddleware. Returns false when Redis is unavailable. */
95
+ async refresh(id: string) {
96
+ const client = redisClientIfReady();
97
+ if (!client) return false;
98
+
99
+ await client.expire(sessionKey(id), env.SESSION_TTL_SECONDS);
100
+ return true;
101
+ },
102
+
103
+ /** Delete a session document from Redis. Use on logout or session expiry. Returns false when Redis is unavailable. */
104
+ async destroy(id: string) {
105
+ const client = redisClientIfReady();
106
+ if (!client) return false;
107
+
108
+ await client.del(sessionKey(id));
109
+ return true;
110
+ },
111
+
112
+ /** Returns true when Redis is enabled and the Redis client is ready — guards against using session store without Redis. */
113
+ isAvailable() {
114
+ return env.REDIS && redisClientIfReady() !== null;
115
+ }
116
+ };