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,24 @@
1
+ {
2
+ "steps": [
3
+ {
4
+ "name": "Generate deploy files",
5
+ "run": "deploy:create --server --force",
6
+ "enabled": false
7
+ },
8
+ {
9
+ "name": "Start shared infra",
10
+ "run": "deploy:server",
11
+ "enabled": true
12
+ },
13
+ {
14
+ "name": "Start app stack",
15
+ "run": "deploy:app",
16
+ "enabled": true
17
+ },
18
+ {
19
+ "name": "Import MySQL dump (optional)",
20
+ "run": "deploy:db:import --file=deploy/nexgen.sql --database=nexgen",
21
+ "enabled": false
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "remote": {
3
+ "host": "127.0.0.1",
4
+ "user": "deploy",
5
+ "port": 22,
6
+ "keyPath": "~/.ssh/id_rsa",
7
+ "targetPath": "/home/deploy/nexgen"
8
+ },
9
+ "upload": {
10
+ "source": ".",
11
+ "targetSubPath": "."
12
+ },
13
+ "databaseImport": {
14
+ "enabled": false,
15
+ "file": "deploy/nexgen.sql",
16
+ "database": "nexgen",
17
+ "container": "mysql-global",
18
+ "user": "root"
19
+ }
20
+ }
@@ -0,0 +1,33 @@
1
+ import { defineSchedule, dispatchEvent } from "@/framework/facade.js";
2
+
3
+ /**
4
+ * name: Unique schedule id used as the distributed lock key.
5
+ * expression: Cron expression that controls when the task runs.
6
+ * timezone: IANA timezone for cron evaluation (example: "UTC").
7
+ * runOnInit: Runs once immediately when scheduler starts.
8
+ * enabled: Enables or disables this schedule without deleting code.
9
+ * ttlMs: Lock TTL in milliseconds to avoid duplicate execution.
10
+ */
11
+ defineSchedule({
12
+ name: "{{module}}:scheduler-tick",
13
+ expression: "*/30 * * * * *",
14
+ // timezone: "UTC",
15
+ runOnInit: true,
16
+ // enabled: true,
17
+ // ttlMs: 120000,
18
+ handler: async () => {
19
+ /**
20
+ * dispatchEvent name: Event id to publish to listeners.
21
+ * payload: Data sent to queue/broadcast consumers.
22
+ * queue: Optional queue target for async event consumers.
23
+ * broadcast: Optional realtime fanout (all/users/roles/rooms).
24
+ */
25
+ await dispatchEvent("{{module}}.scheduler.tick", {
26
+ origin: "scheduler",
27
+ createdAt: new Date().toISOString()
28
+ }, {
29
+ queue: "default",
30
+ broadcast: { all: true }
31
+ });
32
+ }
33
+ });
@@ -0,0 +1,503 @@
1
+ import type { Handler } from "hono";
2
+ import crypto from "node:crypto";
3
+ import { cache, dispatchCommand, dispatchEvent, session, storage } from "@/framework/facade.js";
4
+
5
+ /**
6
+ * Example controller guide:
7
+ * - c.req.valid("json"): reads validated JSON payload from route schema.
8
+ * - dispatchEvent(name, payload, options): emits domain events.
9
+ * - dispatchCommand(name, payload, { async, queue }): queues command-style work.
10
+ * - storage.generateForDownload(...): creates one-time temp download token.
11
+ * - storage.consumeGenerated(token): reads temp file and deletes it.
12
+ * - cache.put/get/remember: lightweight state for async flows (PDF status).
13
+ */
14
+
15
+ type PdfRequest = {
16
+ title: string;
17
+ rows: number;
18
+ chunkSize: number;
19
+ };
20
+
21
+ function pdfStatusKey(requestId: string) {
22
+ return "{{module}}:pdf:status:" + requestId;
23
+ }
24
+
25
+ /**
26
+ * Why: Demonstrates event dispatch with both queue and websocket broadcast.
27
+ * When: Use as the baseline event example for async + realtime flow.
28
+ * Where: POST /{{module}}/event.
29
+ */
30
+ export const fire: Handler = async (c: any) => {
31
+ // Route validates body using ExampleEventSchema in route stub.
32
+ const body = c.req.valid("json");
33
+
34
+ // Session example: keeps last payload for this visitor/session.
35
+ await session.put(c.get("sessionId"), "lastExampleEvent", body);
36
+
37
+ // Event options shown here:
38
+ // - queue: true -> send to default async event queue
39
+ // - broadcast: { all: true } -> fanout to realtime listeners
40
+ await dispatchEvent("{{module}}.created", body, {
41
+ queue: true,
42
+ broadcast: { all: true }
43
+ });
44
+
45
+ return c.json({ message: "Event dispatched", data: body }, 202);
46
+ };
47
+
48
+ /**
49
+ * Why: Demonstrates command-style background processing via queue.
50
+ * When: Use when work should run asynchronously and return 202 immediately.
51
+ * Where: POST /{{module}}/queue/command.
52
+ */
53
+ export const queueCommand: Handler = async (c: any) => {
54
+ const body = c.req.valid("json");
55
+
56
+ // Command options:
57
+ // - async: true -> run in worker, do not block request
58
+ // - queue: "default" -> explicit queue channel
59
+ await dispatchCommand("{{module}}.process", body, {
60
+ async: true,
61
+ queue: "default"
62
+ });
63
+
64
+ return c.json({ message: "Queue command dispatched", data: body }, 202);
65
+ };
66
+
67
+ /**
68
+ * Why: Demonstrates queued events without realtime fanout.
69
+ * When: Use when consumers should process event in workers only.
70
+ * Where: POST /{{module}}/event/queued.
71
+ */
72
+ export const fireQueued: Handler = async (c: any) => {
73
+ const body = c.req.valid("json");
74
+
75
+ await dispatchEvent("{{module}}.created", body, {
76
+ queue: "default"
77
+ });
78
+
79
+ return c.json({ message: "Queued event dispatched", data: body }, 202);
80
+ };
81
+
82
+ /**
83
+ * Why: Demonstrates one event feeding both queue workers and broadcast listeners.
84
+ * When: Use when background processing and client updates are both needed.
85
+ * Where: POST /{{module}}/event/queued-broadcast.
86
+ */
87
+ export const fireQueuedBroadcast: Handler = async (c: any) => {
88
+ const body = c.req.valid("json");
89
+
90
+ await dispatchEvent("{{module}}.created", body, {
91
+ queue: "default",
92
+ broadcast: { all: true }
93
+ });
94
+
95
+ return c.json({ message: "Queued broadcast event dispatched", data: body }, 202);
96
+ };
97
+
98
+ /**
99
+ * Why: Demonstrates broadcast-only realtime events.
100
+ * When: Use for transient notifications with no worker processing.
101
+ * Where: POST /{{module}}/event/broadcast.
102
+ */
103
+ export const fireBroadcast: Handler = async () => {
104
+ const payload = {
105
+ message: "Examples broadcast fired",
106
+ firedAt: new Date().toISOString()
107
+ };
108
+
109
+ await dispatchEvent("{{module}}.realtime", payload, {
110
+ broadcast: { all: true }
111
+ });
112
+
113
+ return Response.json({ message: "Broadcast event dispatched", data: payload });
114
+ };
115
+
116
+ /**
117
+ * Why: Demonstrates multipart upload with storage facade.
118
+ * When: Use when API accepts binary files from forms/clients.
119
+ * Where: POST /{{module}}/upload.
120
+ */
121
+ export const upload: Handler = async (c) => {
122
+ // parseBody() is used for multipart/form-data requests.
123
+ const body = await c.req.parseBody();
124
+ const file = body.file;
125
+
126
+ if (!(file instanceof File)) {
127
+ return c.json({ message: "File is required" }, 422);
128
+ }
129
+
130
+ // Writes to public disk. Name is auto-generated when omitted.
131
+ const path = await storage.disk("public").putFile("uploads", file);
132
+
133
+ return c.json({
134
+ message: "File uploaded successfully",
135
+ path,
136
+ url: storage.disk("public").url(path)
137
+ });
138
+ };
139
+
140
+ /**
141
+ * Why: Demonstrates Excel import by reading uploaded workbook rows.
142
+ * When: Use when clients submit .xlsx files for server-side processing.
143
+ * Where: POST /{{module}}/import/excel.
144
+ */
145
+ export const importExcel: Handler = async (c) => {
146
+ let ExcelJS: any;
147
+ try {
148
+ ({ default: ExcelJS } = await import("exceljs"));
149
+ } catch {
150
+ return c.json({ message: "Missing optional dependency 'exceljs'. Install with: bun add exceljs" }, 500);
151
+ }
152
+
153
+ const body = await c.req.parseBody();
154
+ const file = body.file;
155
+
156
+ if (!(file instanceof File)) {
157
+ return c.json({ message: "File is required" }, 422);
158
+ }
159
+
160
+ const buffer = Buffer.from(await file.arrayBuffer());
161
+ const workbook = new ExcelJS.Workbook();
162
+ await workbook.xlsx.load(buffer);
163
+
164
+ const sheet = workbook.worksheets[0];
165
+ if (!sheet) {
166
+ return c.json({ message: "Workbook has no sheets" }, 422);
167
+ }
168
+
169
+ const headerRow = sheet.getRow(1);
170
+ const headers = headerRow.values
171
+ .slice(1)
172
+ .map((v: unknown) => String(v || "").trim());
173
+
174
+ const rows: Record<string, string | number | null>[] = [];
175
+ sheet.eachRow((row: any, rowNumber: number) => {
176
+ if (rowNumber === 1) return;
177
+ const values = row.values.slice(1);
178
+ const record: Record<string, string | number | null> = {};
179
+ for (let i = 0; i < headers.length; i++) {
180
+ const key = headers[i] || `column_${i + 1}`;
181
+ const value = values[i];
182
+ record[key] = value == null ? null : (typeof value === "object" && "text" in (value as any) ? String((value as any).text) : value);
183
+ }
184
+ rows.push(record);
185
+ });
186
+
187
+ return c.json({
188
+ message: "Excel imported",
189
+ data: {
190
+ sheetName: sheet.name,
191
+ headers,
192
+ totalRows: rows.length,
193
+ preview: rows.slice(0, 20)
194
+ }
195
+ });
196
+ };
197
+
198
+ /**
199
+ * Why: Demonstrates short-lived generated downloads with one-time token consume.
200
+ * When: Use for temporary export artifacts that should not persist.
201
+ * Where: POST /{{module}}/download/generate.
202
+ */
203
+ export const generateDownload: Handler = async () => {
204
+ // Any generated content can be stored as one-time download token.
205
+ const csv = ["id,title", "1,nexgen report", "2,generated and temporary"].join("\n");
206
+ const token = await storage.generateForDownload({
207
+ prefix: "{{module}}-report",
208
+ extension: "csv",
209
+ data: csv
210
+ });
211
+
212
+ return Response.json({
213
+ message: "Temporary file generated",
214
+ token,
215
+ downloadUrl: `/{{module}}/download/${encodeURIComponent(token)}`
216
+ });
217
+ };
218
+
219
+ /**
220
+ * Why: Consumes a temporary token and returns the generated CSV file.
221
+ * When: Called after generateDownload to perform one-time download.
222
+ * Where: GET /{{module}}/download/{token}.
223
+ */
224
+ export const downloadGenerated: Handler = async (c) => {
225
+ const token = decodeURIComponent(c.req.param("token"));
226
+
227
+ try {
228
+ // consumeGenerated() returns bytes and removes temp file immediately.
229
+ const file = await storage.consumeGenerated(token);
230
+ return new Response(file, {
231
+ status: 200,
232
+ headers: {
233
+ "content-type": "text/csv; charset=utf-8",
234
+ "content-disposition": "attachment; filename={{module}}-report.csv"
235
+ }
236
+ });
237
+ } catch {
238
+ return c.json({ message: "Download token expired or invalid" }, 404);
239
+ }
240
+ };
241
+
242
+ /**
243
+ * Why: Demonstrates styled Excel export generation and temporary delivery.
244
+ * When: Use when spreadsheet output with formatting/totals is needed.
245
+ * Where: POST /{{module}}/download/excel/generate.
246
+ */
247
+ export const generateExcelDownload: Handler = async (c: any) => {
248
+ let ExcelJS: any;
249
+ try {
250
+ ({ default: ExcelJS } = await import("exceljs"));
251
+ } catch {
252
+ return c.json({ message: "Missing optional dependency 'exceljs'. Install with: bun add exceljs" }, 500);
253
+ }
254
+
255
+ const body = c.req.valid("json") as { title: string };
256
+ // remember(key, ttl, factory) caches expensive work for repeated requests.
257
+ const rows = await cache.remember("{{module}}:index", 60, async () => [
258
+ { id: 1, title: "nexgen framework ready" },
259
+ { id: 2, title: "modules are auto-discovered" }
260
+ ]);
261
+
262
+ const workbook = new ExcelJS.Workbook();
263
+ const sheet = workbook.addWorksheet("Examples");
264
+ sheet.mergeCells("A1:C1");
265
+ sheet.getCell("A1").value = body.title;
266
+ sheet.getCell("A1").font = { bold: true, size: 14, color: { argb: "FFFFFFFF" } };
267
+ sheet.getCell("A1").alignment = { horizontal: "center", vertical: "middle" };
268
+ sheet.getCell("A1").fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF1F4E78" } };
269
+
270
+ sheet.addRow(["ID", "Title", "Length"]);
271
+ const header = sheet.getRow(2);
272
+ header.font = { bold: true, color: { argb: "FFFFFFFF" } };
273
+ header.alignment = { horizontal: "center" };
274
+ header.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF2E75B6" } };
275
+
276
+ for (const row of rows) {
277
+ sheet.addRow([row.id, row.title, row.title.length]);
278
+ }
279
+
280
+ const dataStart = 3;
281
+ const dataEnd = dataStart + rows.length - 1;
282
+ const subtotalRow = dataEnd + 1;
283
+ const grandTotalRow = subtotalRow + 1;
284
+
285
+ sheet.getCell(`A${subtotalRow}`).value = "Subtotal";
286
+ sheet.getCell(`B${subtotalRow}`).value = rows.length;
287
+ sheet.getCell(`C${subtotalRow}`).value = { formula: `SUM(C${dataStart}:C${dataEnd})` };
288
+ sheet.getCell(`A${grandTotalRow}`).value = "Grand Total";
289
+ sheet.getCell(`B${grandTotalRow}`).value = rows.length;
290
+ sheet.getCell(`C${grandTotalRow}`).value = { formula: `SUM(C${dataStart}:C${dataEnd})` };
291
+
292
+ for (const rowNumber of [subtotalRow, grandTotalRow]) {
293
+ const row = sheet.getRow(rowNumber);
294
+ row.font = { bold: true };
295
+ row.fill = { type: "pattern", pattern: "solid", fgColor: { argb: rowNumber === grandTotalRow ? "FFFCE4D6" : "FFE2EFDA" } };
296
+ }
297
+
298
+ sheet.columns = [{ width: 10 }, { width: 48 }, { width: 14 }];
299
+ sheet.views = [{ state: "frozen", ySplit: 2 }];
300
+
301
+ sheet.eachRow((row) => {
302
+ row.eachCell((cell) => {
303
+ cell.border = {
304
+ top: { style: "thin", color: { argb: "FFBFBFBF" } },
305
+ left: { style: "thin", color: { argb: "FFBFBFBF" } },
306
+ bottom: { style: "thin", color: { argb: "FFBFBFBF" } },
307
+ right: { style: "thin", color: { argb: "FFBFBFBF" } }
308
+ };
309
+ });
310
+ });
311
+
312
+ const buffer = Buffer.from(await workbook.xlsx.writeBuffer());
313
+ const token = await storage.generateForDownload({
314
+ prefix: "{{module}}-report",
315
+ extension: "xlsx",
316
+ data: buffer
317
+ });
318
+
319
+ return c.json({
320
+ message: "Excel generated",
321
+ token,
322
+ downloadUrl: `/{{module}}/download/excel/${encodeURIComponent(token)}`
323
+ });
324
+ };
325
+
326
+ /**
327
+ * Why: Consumes a temporary token and returns the generated XLSX file.
328
+ * When: Called after generateExcelDownload for one-time Excel download.
329
+ * Where: GET /{{module}}/download/excel/{token}.
330
+ */
331
+ export const downloadGeneratedExcel: Handler = async (c) => {
332
+ const token = decodeURIComponent(c.req.param("token"));
333
+
334
+ try {
335
+ const file = await storage.consumeGenerated(token);
336
+ return new Response(file, {
337
+ status: 200,
338
+ headers: {
339
+ "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
340
+ "content-disposition": "attachment; filename={{module}}-report.xlsx"
341
+ }
342
+ });
343
+ } catch {
344
+ return c.json({ message: "Download token expired or invalid" }, 404);
345
+ }
346
+ };
347
+
348
+ /**
349
+ * Why: Starts hybrid PDF generation for heavy design + large data.
350
+ * When: Use when reports should be rendered with HTML/CSS and processed in background.
351
+ * Where: POST /{{module}}/download/pdf/generate.
352
+ */
353
+ export const generatePdfDownload: Handler = async (c: any) => {
354
+ const body = c.req.valid("json") as PdfRequest;
355
+ const requestId = crypto.randomUUID();
356
+
357
+ // Queue mode path: store status in cache, then worker updates it.
358
+ if (cache.isAvailable()) {
359
+ await cache.put(pdfStatusKey(requestId), {
360
+ state: "pending",
361
+ createdAt: new Date().toISOString(),
362
+ mode: "queued"
363
+ }, 1800);
364
+
365
+ await dispatchCommand("{{module}}.pdf.generate", {
366
+ requestId,
367
+ ...body
368
+ }, {
369
+ async: true,
370
+ queue: "default"
371
+ });
372
+
373
+ return c.json({
374
+ message: "PDF generation queued",
375
+ requestId,
376
+ mode: "queued",
377
+ statusUrl: `/{{module}}/download/pdf/${requestId}`,
378
+ downloadUrl: `/{{module}}/download/pdf/${requestId}`
379
+ }, 202);
380
+ }
381
+
382
+ const token = await generatePdfToken(body);
383
+ return c.json({
384
+ message: "PDF generated",
385
+ requestId,
386
+ mode: "inline",
387
+ statusUrl: `/{{module}}/download/pdf/${requestId}`,
388
+ downloadUrl: `/{{module}}/download/pdf/${encodeURIComponent(token)}`
389
+ }, 202);
390
+ };
391
+
392
+ /**
393
+ * Why: Polls queued PDF status, then serves one-time PDF download when ready.
394
+ * When: Use after generatePdfDownload returns queued request id.
395
+ * Where: GET /{{module}}/download/pdf/{requestId}.
396
+ */
397
+ export const downloadGeneratedPdf: Handler = async (c) => {
398
+ const requestId = decodeURIComponent(c.req.param("requestId"));
399
+
400
+ if (cache.isAvailable()) {
401
+ // Status API contract:
402
+ // - pending -> return 202
403
+ // - ready + token -> return file
404
+ // - failed -> return 500 with message
405
+ const status = await cache.get<{ state: string; token?: string; message?: string }>(pdfStatusKey(requestId));
406
+
407
+ if (!status) return c.json({ message: "Unknown PDF request id" }, 404);
408
+ if (status.state === "pending") return c.json({ message: "PDF is still generating", state: "pending" }, 202);
409
+ if (status.state === "failed") return c.json({ message: status.message || "PDF generation failed" }, 500);
410
+
411
+ if (status.token) {
412
+ try {
413
+ const file = await storage.consumeGenerated(status.token);
414
+ await cache.forget(pdfStatusKey(requestId));
415
+ return new Response(file, {
416
+ status: 200,
417
+ headers: {
418
+ "content-type": "application/pdf",
419
+ "content-disposition": "attachment; filename={{module}}-report.pdf"
420
+ }
421
+ });
422
+ } catch {
423
+ return c.json({ message: "PDF token expired or invalid" }, 404);
424
+ }
425
+ }
426
+ }
427
+
428
+ try {
429
+ const file = await storage.consumeGenerated(requestId);
430
+ return new Response(file, {
431
+ status: 200,
432
+ headers: {
433
+ "content-type": "application/pdf",
434
+ "content-disposition": "attachment; filename={{module}}-report.pdf"
435
+ }
436
+ });
437
+ } catch {
438
+ return c.json({ message: "PDF token expired or invalid" }, 404);
439
+ }
440
+ };
441
+
442
+ export async function generatePdfToken(payload: PdfRequest) {
443
+ let chromium: any;
444
+ try {
445
+ ({ chromium } = await import("playwright"));
446
+ } catch {
447
+ throw new Error("Missing optional dependency 'playwright'. Install with: bun add playwright and run: bunx playwright install chromium");
448
+ }
449
+ const browser = await chromium.launch({ headless: true });
450
+
451
+ try {
452
+ const page = await browser.newPage();
453
+ await page.setContent(buildPdfHtml(payload), { waitUntil: "networkidle" });
454
+ const pdf = await page.pdf({
455
+ format: "A4",
456
+ printBackground: true,
457
+ margin: { top: "16mm", right: "12mm", bottom: "16mm", left: "12mm" }
458
+ });
459
+
460
+ return await storage.generateForDownload({
461
+ prefix: "{{module}}-report",
462
+ extension: "pdf",
463
+ data: pdf
464
+ });
465
+ } finally {
466
+ await browser.close();
467
+ }
468
+ }
469
+
470
+ function buildPdfHtml(payload: PdfRequest) {
471
+ const rows = Array.from({ length: payload.rows }, (_, i) => ({
472
+ id: i + 1,
473
+ label: `Record ${i + 1}`,
474
+ value: ((i + 1) * 17) % 1000
475
+ }));
476
+ const chunks: typeof rows[] = [];
477
+
478
+ for (let i = 0; i < rows.length; i += payload.chunkSize) {
479
+ chunks.push(rows.slice(i, i + payload.chunkSize));
480
+ }
481
+
482
+ const sections = chunks.map((chunk, index) => {
483
+ const trs = chunk.map(item => `<tr><td>${item.id}</td><td>${item.label}</td><td>${item.value}</td></tr>`).join("");
484
+ return `<section class="page"><h2>Batch ${index + 1}</h2><table><thead><tr><th>ID</th><th>Label</th><th>Value</th></tr></thead><tbody>${trs}</tbody></table></section>`;
485
+ }).join("");
486
+
487
+ return `<!doctype html><html><head><meta charset="utf-8" /><style>
488
+ body{font-family:Arial,sans-serif;color:#1f2937;margin:0}
489
+ .hero{padding:24px;background:linear-gradient(120deg,#0f766e,#1d4ed8);color:#fff}
490
+ .hero h1{margin:0 0 4px 0;font-size:22px}
491
+ .hero p{margin:0;font-size:12px;opacity:.9}
492
+ .page{padding:16px 20px;page-break-after:always}
493
+ .page:last-child{page-break-after:auto}
494
+ h2{margin:0 0 10px 0;font-size:14px;color:#0f172a}
495
+ table{width:100%;border-collapse:collapse;font-size:11px}
496
+ th,td{border:1px solid #cbd5e1;padding:6px}
497
+ th{background:#e2e8f0;text-align:left}
498
+ tr:nth-child(even){background:#f8fafc}
499
+ </style></head><body>
500
+ <div class="hero"><h1>${payload.title}</h1><p>Rows: ${payload.rows} | Chunk size: ${payload.chunkSize}</p></div>
501
+ ${sections}
502
+ </body></html>`;
503
+ }
@@ -0,0 +1,74 @@
1
+ import { cache, dispatchEvent, shouldQueue } from "@/framework/facade.js";
2
+ import { generatePdfToken } from "@/modules/{{module}}/controllers/{{module}}.controller.js";
3
+
4
+ /**
5
+ * event: Event name this worker listens to.
6
+ * queue: Queue name where jobs are consumed from.
7
+ * handler: Async processor that receives `job.data`.
8
+ */
9
+ shouldQueue("{{module}}.process", "default", async (job) => {
10
+ const waitSeconds = Number(job.data.waitSeconds || 1);
11
+ await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
12
+
13
+ await cache.put("{{module}}:last-command-job", {
14
+ payload: job.data,
15
+ handledAt: new Date().toISOString()
16
+ }, 120);
17
+
18
+ /**
19
+ * dispatchEvent name: Event id to publish to listeners.
20
+ * payload: Data sent to queue/broadcast consumers.
21
+ * queue: Optional queue target for async event consumers.
22
+ * broadcast: Optional realtime fanout (all/users/roles/rooms).
23
+ */
24
+ await dispatchEvent("{{module}}.process.finished", {
25
+ ...job.data,
26
+ handledAt: new Date().toISOString()
27
+ }, {
28
+ broadcast: { all: true }
29
+ });
30
+
31
+ return { ok: true, mode: "queue" };
32
+ });
33
+
34
+ shouldQueue("{{module}}.created", "default", async (job) => {
35
+ await cache.put("{{module}}:last-created-event", job.data, 120);
36
+ return { ok: true, mode: "event-queue" };
37
+ });
38
+
39
+ shouldQueue("{{module}}.scheduler.tick", "default", async (job) => {
40
+ await cache.put("{{module}}:last-scheduler-job", job.data, 120);
41
+ return { ok: true, mode: "scheduler-queue" };
42
+ });
43
+
44
+ shouldQueue("{{module}}.pdf.generate", "default", async (job) => {
45
+ const requestId = String(job.data.requestId || "");
46
+
47
+ if (!requestId) {
48
+ return { ok: false, mode: "pdf-queue", message: "Missing requestId" };
49
+ }
50
+
51
+ const key = `{{module}}:pdf:status:${requestId}`;
52
+
53
+ try {
54
+ const token = await generatePdfToken({
55
+ title: String(job.data.title || "Example PDF Report"),
56
+ rows: Number(job.data.rows || 1000),
57
+ chunkSize: Number(job.data.chunkSize || 250)
58
+ });
59
+
60
+ await cache.put(key, {
61
+ state: "ready",
62
+ token,
63
+ readyAt: new Date().toISOString()
64
+ }, 1800);
65
+
66
+ return { ok: true, mode: "pdf-queue" };
67
+ } catch (error) {
68
+ await cache.put(key, {
69
+ state: "failed",
70
+ message: error instanceof Error ? error.message : "Unknown error"
71
+ }, 1800);
72
+ return { ok: false, mode: "pdf-queue" };
73
+ }
74
+ });