create-nexwire 4.3.0 → 4.4.0

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 (326) hide show
  1. package/README.md +10 -5
  2. package/express/src/framework/facade.ts +1 -0
  3. package/express/src/framework/http/logger.ts +13 -4
  4. package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
  5. package/express/src/framework/notification/channels/mail.ts +8 -6
  6. package/express/src/middlewares/auth-middleware.ts +1 -2
  7. package/express/src/middlewares/role-middleware.ts +3 -13
  8. package/express/src/modules/auth/routes/index.ts +1 -2
  9. package/express/src/providers/notification.ts +2 -2
  10. package/express/src/resources/src/components/Switch.vue +60 -65
  11. package/express/src/resources/src/pages/auth/forgetPassword.vue +23 -30
  12. package/express/src/resources/src/pages/auth/login.vue +25 -28
  13. package/express/src/resources/src/pages/auth/register.vue +35 -39
  14. package/express/src/resources/src/pages/auth/resetPassword.vue +29 -37
  15. package/express/src/resources/src/pages/auth/verifyEmail.vue +15 -19
  16. package/express/src/resources/src/plugins/gum.ts +519 -203
  17. package/express/src/resources/src/stores/auth.ts +9 -6
  18. package/express/src/storage/database/nexwire.sqlite +0 -0
  19. package/fastify/.dockerignore +14 -0
  20. package/fastify/.env +44 -0
  21. package/fastify/.env.example +42 -0
  22. package/fastify/.vscode/extensions.json +6 -0
  23. package/fastify/.vscode/settings.json +37 -0
  24. package/fastify/LICENSE +21 -0
  25. package/fastify/README.md +232 -0
  26. package/fastify/biome.json +99 -0
  27. package/fastify/drizzle.config.ts +25 -0
  28. package/fastify/gitignore-stub +31 -0
  29. package/fastify/package.json +130 -0
  30. package/fastify/public/.gitkeep +0 -0
  31. package/fastify/src/config/app.ts +41 -0
  32. package/fastify/src/config/auth.ts +12 -0
  33. package/fastify/src/config/cache.ts +16 -0
  34. package/fastify/src/config/cookie.ts +15 -0
  35. package/fastify/src/config/cors.ts +12 -0
  36. package/fastify/src/config/database.ts +48 -0
  37. package/fastify/src/config/index.ts +83 -0
  38. package/fastify/src/config/jwt.ts +19 -0
  39. package/fastify/src/config/logging.ts +13 -0
  40. package/fastify/src/config/mail.ts +24 -0
  41. package/fastify/src/config/openapi.ts +36 -0
  42. package/fastify/src/config/queue.ts +30 -0
  43. package/fastify/src/config/rateLimit.ts +17 -0
  44. package/fastify/src/config/realtime.ts +15 -0
  45. package/fastify/src/config/redis.ts +18 -0
  46. package/fastify/src/config/security.ts +44 -0
  47. package/fastify/src/config/session.ts +17 -0
  48. package/fastify/src/config/storage.ts +22 -0
  49. package/fastify/src/config/validate.ts +50 -0
  50. package/fastify/src/database/migrations/sqlite/0000_init.sql +48 -0
  51. package/fastify/src/database/migrations/sqlite/meta/0000_snapshot.json +324 -0
  52. package/fastify/src/database/migrations/sqlite/meta/_journal.json +13 -0
  53. package/fastify/src/database/schema.ts +2 -0
  54. package/fastify/src/env.ts +76 -0
  55. package/fastify/src/framework/cache/cache.ts +114 -0
  56. package/fastify/src/framework/circuit-breaker/cb.ts +88 -0
  57. package/fastify/src/framework/database/app-schema.ts +37 -0
  58. package/fastify/src/framework/database/connection.ts +223 -0
  59. package/fastify/src/framework/database/migrate-hooks.ts +87 -0
  60. package/fastify/src/framework/database/paginate.ts +226 -0
  61. package/fastify/src/framework/database/schema.ts +20 -0
  62. package/fastify/src/framework/database/seed.ts +152 -0
  63. package/fastify/src/framework/events/dispatcher.ts +61 -0
  64. package/fastify/src/framework/facade.ts +96 -0
  65. package/fastify/src/framework/http/app.ts +176 -0
  66. package/fastify/src/framework/http/cors.ts +13 -0
  67. package/fastify/src/framework/http/fastify.d.ts +13 -0
  68. package/fastify/src/framework/http/logger.ts +110 -0
  69. package/fastify/src/framework/http/metrics.ts +22 -0
  70. package/fastify/src/framework/http/openapi.ts +48 -0
  71. package/fastify/src/framework/http/ratelimiter.ts +47 -0
  72. package/fastify/src/framework/http/security.ts +25 -0
  73. package/fastify/src/framework/http/static.ts +83 -0
  74. package/fastify/src/framework/http/validation.ts +49 -0
  75. package/fastify/src/framework/kernel.ts +43 -0
  76. package/fastify/src/framework/maker-cli/db/core.mjs +291 -0
  77. package/fastify/src/framework/maker-cli/db/index.mjs +204 -0
  78. package/fastify/src/framework/maker-cli/deploy/core.mjs +1046 -0
  79. package/fastify/src/framework/maker-cli/deploy/index.mjs +91 -0
  80. package/fastify/src/framework/maker-cli/index.mjs +65 -0
  81. package/fastify/src/framework/maker-cli/middleware/core.mjs +41 -0
  82. package/fastify/src/framework/maker-cli/middleware/index.mjs +17 -0
  83. package/fastify/src/framework/maker-cli/module/core.mjs +1066 -0
  84. package/fastify/src/framework/maker-cli/module/index.mjs +211 -0
  85. package/fastify/src/framework/maker-cli/provider/core.mjs +49 -0
  86. package/fastify/src/framework/maker-cli/provider/index.mjs +12 -0
  87. package/fastify/src/framework/maker-cli/runtime/build-ui.mjs +16 -0
  88. package/fastify/src/framework/maker-cli/runtime/core.mjs +560 -0
  89. package/fastify/src/framework/maker-cli/runtime/index.mjs +120 -0
  90. package/fastify/src/framework/maker-cli/runtime/redis-commander-patch.cjs +20 -0
  91. package/fastify/src/framework/maker-cli/stubs/controller/openapi.ts.stub +74 -0
  92. package/fastify/src/framework/maker-cli/stubs/controller/plain.ts.stub +77 -0
  93. package/fastify/src/framework/maker-cli/stubs/controller/standalone.ts.stub +51 -0
  94. package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +52 -0
  95. package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +55 -0
  96. package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +51 -0
  97. package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +55 -0
  98. package/fastify/src/framework/maker-cli/stubs/deploy/README.stub +59 -0
  99. package/fastify/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +45 -0
  100. package/fastify/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +45 -0
  101. package/fastify/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +45 -0
  102. package/fastify/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +68 -0
  103. package/fastify/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +68 -0
  104. package/fastify/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +67 -0
  105. package/fastify/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +41 -0
  106. package/fastify/src/framework/maker-cli/stubs/deploy/server/README.stub +78 -0
  107. package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +120 -0
  108. package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +133 -0
  109. package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +131 -0
  110. package/fastify/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +11 -0
  111. package/fastify/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +25 -0
  112. package/fastify/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +25 -0
  113. package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +16 -0
  114. package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +13 -0
  115. package/fastify/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +14 -0
  116. package/fastify/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +7 -0
  117. package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +54 -0
  118. package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +70 -0
  119. package/fastify/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +25 -0
  120. package/fastify/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +24 -0
  121. package/fastify/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  122. package/fastify/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  123. package/fastify/src/framework/maker-cli/stubs/job/name.ts.stub +25 -0
  124. package/fastify/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  125. package/fastify/src/framework/maker-cli/stubs/middleware/name.ts.stub +15 -0
  126. package/fastify/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +10 -0
  127. package/fastify/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +10 -0
  128. package/fastify/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +10 -0
  129. package/fastify/src/framework/maker-cli/stubs/notification/controller.ts.stub +79 -0
  130. package/fastify/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -0
  131. package/fastify/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
  132. package/fastify/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
  133. package/fastify/src/framework/maker-cli/stubs/notification/route.api.ts.stub +71 -0
  134. package/fastify/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +15 -0
  135. package/fastify/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +26 -0
  136. package/fastify/src/framework/maker-cli/stubs/notification/schema.ts.stub +26 -0
  137. package/fastify/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
  138. package/fastify/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
  139. package/fastify/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
  140. package/fastify/src/framework/maker-cli/stubs/route/api.ts.stub +101 -0
  141. package/fastify/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -0
  142. package/fastify/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +120 -0
  143. package/fastify/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +50 -0
  144. package/fastify/src/framework/maker-cli/stubs/schedule/name.ts.stub +36 -0
  145. package/fastify/src/framework/maker-cli/stubs/schema/name.plain.ts.stub +14 -0
  146. package/fastify/src/framework/maker-cli/stubs/schema/name.ts.stub +33 -0
  147. package/fastify/src/framework/maker-cli/stubs/seeder/name.ts.stub +18 -0
  148. package/fastify/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
  149. package/fastify/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  150. package/fastify/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  151. package/fastify/src/framework/maker-cli/stubs/test/unit.ts.stub +22 -0
  152. package/fastify/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  153. package/fastify/src/framework/maker-cli/utils/env-config.mjs +44 -0
  154. package/fastify/src/framework/maker-cli/utils/env-db.mjs +61 -0
  155. package/fastify/src/framework/maker-cli/utils/file-ops.mjs +30 -0
  156. package/fastify/src/framework/maker-cli/utils/flags.mjs +14 -0
  157. package/fastify/src/framework/maker-cli/utils/help.mjs +379 -0
  158. package/fastify/src/framework/maker-cli/utils/naming.mjs +119 -0
  159. package/fastify/src/framework/maker-cli/utils/process.mjs +52 -0
  160. package/fastify/src/framework/maker-cli/utils/project-commands.mjs +201 -0
  161. package/fastify/src/framework/modules/discover.ts +57 -0
  162. package/fastify/src/framework/modules/routes.ts +31 -0
  163. package/fastify/src/framework/notification/channels/broadcast.ts +15 -0
  164. package/fastify/src/framework/notification/channels/mail.ts +42 -0
  165. package/fastify/src/framework/notification/contracts.ts +29 -0
  166. package/fastify/src/framework/notification/index.ts +59 -0
  167. package/fastify/src/framework/notification/registry.ts +15 -0
  168. package/fastify/src/framework/providers/adapter.ts +149 -0
  169. package/fastify/src/framework/providers/contracts.ts +126 -0
  170. package/fastify/src/framework/providers/loader.ts +105 -0
  171. package/fastify/src/framework/providers/registry.ts +88 -0
  172. package/fastify/src/framework/queue/clear.ts +22 -0
  173. package/fastify/src/framework/queue/queue.ts +401 -0
  174. package/fastify/src/framework/queue/ui.ts +194 -0
  175. package/fastify/src/framework/queue/worker.ts +61 -0
  176. package/fastify/src/framework/realtime/broadcast.ts +70 -0
  177. package/fastify/src/framework/realtime/index.ts +2 -0
  178. package/fastify/src/framework/realtime/socket-cookie.ts +72 -0
  179. package/fastify/src/framework/realtime/socket.ts +149 -0
  180. package/fastify/src/framework/realtime/types.ts +6 -0
  181. package/fastify/src/framework/realtime/ui.ts +16 -0
  182. package/fastify/src/framework/redis/client.ts +148 -0
  183. package/fastify/src/framework/runtime/runtime.ts +23 -0
  184. package/fastify/src/framework/runtime/server-adapter.ts +33 -0
  185. package/fastify/src/framework/scheduler/lock.ts +128 -0
  186. package/fastify/src/framework/scheduler/run.ts +48 -0
  187. package/fastify/src/framework/scheduler/scheduler.ts +155 -0
  188. package/fastify/src/framework/server.ts +162 -0
  189. package/fastify/src/framework/session/session.ts +160 -0
  190. package/fastify/src/framework/storage/storage.ts +744 -0
  191. package/fastify/src/framework/support/cookie.ts +71 -0
  192. package/fastify/src/framework/support/jwt.ts +33 -0
  193. package/fastify/src/framework/support/lifecycle.ts +35 -0
  194. package/fastify/src/framework/support/logger.ts +148 -0
  195. package/fastify/src/framework/support/mail.ts +56 -0
  196. package/fastify/src/framework/support/password.ts +23 -0
  197. package/fastify/src/framework/support/status-codes.ts +75 -0
  198. package/fastify/src/framework/support/upload.ts +181 -0
  199. package/fastify/src/framework/support/url.ts +41 -0
  200. package/fastify/src/middlewares/auth-middleware.ts +134 -0
  201. package/fastify/src/middlewares/role-middleware.ts +19 -0
  202. package/fastify/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
  203. package/fastify/src/modules/auth/__tests__/login.test.ts +171 -0
  204. package/fastify/src/modules/auth/__tests__/logout.test.ts +60 -0
  205. package/fastify/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
  206. package/fastify/src/modules/auth/__tests__/me.test.ts +91 -0
  207. package/fastify/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  208. package/fastify/src/modules/auth/__tests__/register.test.ts +291 -0
  209. package/fastify/src/modules/auth/__tests__/resetpass.test.ts +206 -0
  210. package/fastify/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
  211. package/fastify/src/modules/auth/controllers/auth.ts +252 -0
  212. package/fastify/src/modules/auth/controllers/role.ts +35 -0
  213. package/fastify/src/modules/auth/database/models/role.ts +14 -0
  214. package/fastify/src/modules/auth/database/models/user.ts +46 -0
  215. package/fastify/src/modules/auth/database/seeders/role.ts +18 -0
  216. package/fastify/src/modules/auth/database/seeders/user.ts +35 -0
  217. package/fastify/src/modules/auth/facade.ts +12 -0
  218. package/fastify/src/modules/auth/helpers/auth.ts +115 -0
  219. package/fastify/src/modules/auth/jobs/forgetpass.ts +20 -0
  220. package/fastify/src/modules/auth/jobs/registeruser.ts +23 -0
  221. package/fastify/src/modules/auth/jobs/verifyemail.ts +18 -0
  222. package/fastify/src/modules/auth/routes/index.ts +185 -0
  223. package/fastify/src/modules/auth/routes/role.ts +56 -0
  224. package/fastify/src/modules/auth/schemas/auth.ts +116 -0
  225. package/fastify/src/modules/auth/services/forgetpassword.ts +66 -0
  226. package/fastify/src/modules/auth/services/login.ts +63 -0
  227. package/fastify/src/modules/auth/services/logout.ts +25 -0
  228. package/fastify/src/modules/auth/services/logoutalldevices.ts +19 -0
  229. package/fastify/src/modules/auth/services/me.ts +31 -0
  230. package/fastify/src/modules/auth/services/refreshtoken.ts +110 -0
  231. package/fastify/src/modules/auth/services/register.ts +141 -0
  232. package/fastify/src/modules/auth/services/resetpassword.ts +69 -0
  233. package/fastify/src/modules/auth/services/verifyemail.ts +77 -0
  234. package/fastify/src/modules/auth/types/auth.ts +31 -0
  235. package/fastify/src/providers/README.md +107 -0
  236. package/fastify/src/providers/notification.ts +4 -0
  237. package/fastify/src/resources/index.html +17 -0
  238. package/fastify/src/resources/src/App.vue +5 -0
  239. package/fastify/src/resources/src/assets/css/styles.css +14761 -0
  240. package/fastify/src/resources/src/assets/css/styles.css.map +1 -0
  241. package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin-ext.woff2 +0 -0
  242. package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin.woff2 +0 -0
  243. package/fastify/src/resources/src/assets/images/favicon/favicon.ico +0 -0
  244. package/fastify/src/resources/src/assets/images/logo.png +0 -0
  245. package/fastify/src/resources/src/assets/scss/custom.css +217 -0
  246. package/fastify/src/resources/src/assets/scss/custom.css.map +1 -0
  247. package/fastify/src/resources/src/assets/scss/custom.scss +1346 -0
  248. package/fastify/src/resources/src/components/Button.vue +32 -0
  249. package/fastify/src/resources/src/components/Checkbox.vue +32 -0
  250. package/fastify/src/resources/src/components/Datepicker.vue +106 -0
  251. package/fastify/src/resources/src/components/FeatureButton.vue +53 -0
  252. package/fastify/src/resources/src/components/FloatButton.vue +33 -0
  253. package/fastify/src/resources/src/components/Href.vue +32 -0
  254. package/fastify/src/resources/src/components/Input.vue +243 -0
  255. package/fastify/src/resources/src/components/InputGroup.vue +147 -0
  256. package/fastify/src/resources/src/components/InputPasswordToggle.vue +229 -0
  257. package/fastify/src/resources/src/components/Modal.vue +71 -0
  258. package/fastify/src/resources/src/components/Pagebar.vue +26 -0
  259. package/fastify/src/resources/src/components/Refresh.vue +19 -0
  260. package/fastify/src/resources/src/components/Select.vue +384 -0
  261. package/fastify/src/resources/src/components/Spinner.vue +42 -0
  262. package/fastify/src/resources/src/components/Switch.vue +60 -0
  263. package/fastify/src/resources/src/components/TextArea.vue +117 -0
  264. package/fastify/src/resources/src/components/Toast.vue +56 -0
  265. package/fastify/src/resources/src/components/datatable/DataTableSkeleton.vue +98 -0
  266. package/fastify/src/resources/src/components/datatable/Pagination.vue +147 -0
  267. package/fastify/src/resources/src/components/datatable/SelectOpption.vue +64 -0
  268. package/fastify/src/resources/src/components/datatable/index.vue +199 -0
  269. package/fastify/src/resources/src/composables/useAuth.ts +52 -0
  270. package/fastify/src/resources/src/helpers/assets.ts +51 -0
  271. package/fastify/src/resources/src/helpers/nformatter.ts +10 -0
  272. package/fastify/src/resources/src/helpers/utils.ts +75 -0
  273. package/fastify/src/resources/src/layouts/AuthLayout.vue +20 -0
  274. package/fastify/src/resources/src/layouts/Layout/Footer.vue +23 -0
  275. package/fastify/src/resources/src/layouts/Layout/Header.vue +124 -0
  276. package/fastify/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
  277. package/fastify/src/resources/src/layouts/Layout/index.vue +87 -0
  278. package/fastify/src/resources/src/main.ts +28 -0
  279. package/fastify/src/resources/src/pages/auth/forgetPassword.vue +76 -0
  280. package/fastify/src/resources/src/pages/auth/login.vue +96 -0
  281. package/fastify/src/resources/src/pages/auth/register.vue +129 -0
  282. package/fastify/src/resources/src/pages/auth/resetPassword.vue +110 -0
  283. package/fastify/src/resources/src/pages/auth/verifyEmail.vue +64 -0
  284. package/fastify/src/resources/src/pages/dashboard/index.vue +66 -0
  285. package/fastify/src/resources/src/plugins/axios.ts +31 -0
  286. package/fastify/src/resources/src/plugins/browserDetect.ts +55 -0
  287. package/fastify/src/resources/src/plugins/dialog.ts +216 -0
  288. package/fastify/src/resources/src/plugins/gum.ts +683 -0
  289. package/fastify/src/resources/src/plugins/pulse.ts +157 -0
  290. package/fastify/src/resources/src/plugins/routeProgress.ts +87 -0
  291. package/fastify/src/resources/src/router/index.ts +85 -0
  292. package/fastify/src/resources/src/stores/admin-ui.ts +148 -0
  293. package/fastify/src/resources/src/stores/auth.ts +187 -0
  294. package/fastify/src/resources/tsconfig.json +19 -0
  295. package/fastify/src/resources/vite.config.ts +64 -0
  296. package/fastify/src/storage/app/private/.gitkeep +0 -0
  297. package/fastify/src/storage/app/public/.gitkeep +0 -0
  298. package/fastify/src/storage/app/tmp/generated/.gitkeep +0 -0
  299. package/fastify/src/storage/database/nexwire.sqlite +0 -0
  300. package/fastify/src/storage/logs/.gitkeep +0 -0
  301. package/fastify/src/storage/logs/app.log +0 -0
  302. package/fastify/src/storage/logs/fatal.log +0 -0
  303. package/fastify/src/storage/tmp/.gitkeep +0 -0
  304. package/fastify/src/storage/trash/modules/.gitkeep +0 -0
  305. package/fastify/src/types/global.d.ts +32 -0
  306. package/fastify/tsconfig.json +19 -0
  307. package/fastify/vitest.config.ts +38 -0
  308. package/hono/src/framework/facade.ts +1 -0
  309. package/hono/src/framework/http/logger.ts +86 -81
  310. package/hono/src/framework/maker-cli/runtime/core.mjs +1 -2
  311. package/hono/src/framework/notification/channels/mail.ts +8 -6
  312. package/hono/src/middlewares/auth-middleware.ts +1 -2
  313. package/hono/src/middlewares/role-middleware.ts +3 -13
  314. package/hono/src/modules/auth/routes/index.ts +1 -2
  315. package/hono/src/providers/notification.ts +2 -2
  316. package/hono/src/resources/src/components/Switch.vue +60 -65
  317. package/hono/src/resources/src/pages/auth/forgetPassword.vue +23 -29
  318. package/hono/src/resources/src/pages/auth/login.vue +26 -29
  319. package/hono/src/resources/src/pages/auth/register.vue +36 -40
  320. package/hono/src/resources/src/pages/auth/resetPassword.vue +29 -37
  321. package/hono/src/resources/src/pages/auth/verifyEmail.vue +15 -19
  322. package/hono/src/resources/src/plugins/gum.ts +519 -203
  323. package/hono/src/resources/src/stores/auth.ts +9 -6
  324. package/hono/src/storage/database/nexwire.sqlite +0 -0
  325. package/package.json +3 -2
  326. package/src/index.js +11 -8
@@ -4,13 +4,38 @@ import type { App } from "vue";
4
4
  import { computed, nextTick, reactive, ref, toRaw } from "vue";
5
5
  import { type LocationQueryRaw, useRoute, useRouter } from "vue-router";
6
6
 
7
+ /* -------------------------------------------------------------------------- */
8
+ /* Types */
9
+ /* -------------------------------------------------------------------------- */
10
+
7
11
  export type GumPluginOptions = {
8
12
  rememberPrefix?: string;
9
13
  recentlySuccessfulDuration?: number;
10
14
  };
11
15
 
12
16
  type GumMethod = "get" | "post" | "put" | "patch" | "delete";
13
- type GumVisitOptions = {
17
+ type FormMethod = "post" | "put" | "patch" | "delete";
18
+
19
+ type LifecycleHooks = {
20
+ onBefore?: () => boolean | undefined | Promise<boolean | undefined>;
21
+ onStart?: () => void | Promise<void>;
22
+ onProgress?: (event: AxiosProgressEvent) => void;
23
+ onSuccess?: (response: AxiosResponse) => void | Promise<void>;
24
+ onError?: (errors: NormalizedErrors, error: AxiosError) => void | Promise<void>;
25
+ onFinish?: () => void | Promise<void>;
26
+ };
27
+
28
+ /**
29
+ * A fetcher returns either an AxiosResponse or any plain payload.
30
+ * Gum wraps plain values into `{ data, status: 200 }`, so store methods
31
+ * returning `Promise<string>` / `Promise<User>` work without a wrapper.
32
+ *
33
+ * Typing is intentionally loose (`Promise<unknown>`); callers should not
34
+ * rely on the fetcher's return type being narrow.
35
+ */
36
+ type Fetcher = (payload: Record<string, unknown> | FormData) => Promise<unknown>;
37
+
38
+ type GumVisitOptions = LifecycleHooks & {
14
39
  method?: GumMethod;
15
40
  data?: Record<string, unknown> | FormData;
16
41
  query?: Record<string, unknown>;
@@ -19,67 +44,44 @@ type GumVisitOptions = {
19
44
  preserveState?: boolean;
20
45
  preserveScroll?: boolean;
21
46
  skipFetch?: boolean;
22
- onBefore?: () => boolean | undefined | Promise<boolean | undefined>;
23
- onStart?: () => void | Promise<void>;
24
- onProgress?: (event: AxiosProgressEvent) => void;
25
- onSuccess?: (response: AxiosResponse) => void | Promise<void>;
26
- onError?: (errors: NormalizedErrors, error: AxiosError) => void | Promise<void>;
27
- onFinish?: () => void | Promise<void>;
47
+ fetcher?: Fetcher;
28
48
  };
29
49
 
30
- type FormMethod = "post" | "put" | "patch" | "delete";
50
+ /** URL-less visit: fetcher REQUIRED, method injected by the helper. */
51
+ type VisitWithFetcherOptions = Omit<GumVisitOptions, "method" | "fetcher"> & { fetcher: Fetcher };
52
+ /** For body-carrying methods (post/put/patch/delete). */
53
+ type VisitWithFetcherDataOptions = VisitWithFetcherOptions & {
54
+ data?: Record<string, unknown> | FormData;
55
+ };
56
+
57
+ type FormSubmitOptions<T> = LifecycleHooks & {
58
+ validate?: (data: T) => Record<string, string> | null | undefined;
59
+ transform?: (data: T) => Record<string, unknown> | FormData;
60
+ fetcher?: Fetcher;
61
+ };
62
+
63
+ /** URL-less form submit: fetcher REQUIRED, method injected by the helper. */
64
+ type FormWithFetcherOptions<T> = Omit<FormSubmitOptions<T>, "fetcher"> & { fetcher: Fetcher };
65
+
31
66
  type FormErrors<T> = Partial<Record<keyof T | string, string>>;
32
67
  type NormalizedErrors = Record<string, string[] | string>;
33
- type FormSubmitOptions = {
34
- onBefore?: () => boolean | undefined | Promise<boolean | undefined>;
35
- onStart?: () => void | Promise<void>;
36
- onProgress?: (event: AxiosProgressEvent) => void | Promise<void>;
37
- onSuccess?: (response: AxiosResponse) => void | Promise<void>;
38
- onError?: (errors: NormalizedErrors, error: AxiosError) => void | Promise<void>;
39
- onFinish?: () => void | Promise<void>;
40
- };
41
68
 
42
69
  type ValidationErrorPayload = {
43
70
  errors?: Record<string, string[] | string>;
44
- error?: {
45
- issues?: Array<{
46
- path?: Array<string | number>;
47
- message?: string;
48
- }>;
49
- };
71
+ error?: { issues?: Array<{ path?: Array<string | number>; message?: string }> };
50
72
  };
51
73
 
52
- function normalizeValidationErrors(payload?: ValidationErrorPayload): NormalizedErrors {
53
- const normalized: NormalizedErrors = { ...payload?.errors };
54
-
55
- const issues = payload?.error?.issues;
56
- if (Array.isArray(issues)) {
57
- issues.forEach((issue) => {
58
- const key = issue.path?.map(String).join(".");
59
- if (key) normalized[key] = String(issue.message || "Invalid value");
60
- });
61
- }
62
-
63
- return normalized;
64
- }
74
+ /* -------------------------------------------------------------------------- */
75
+ /* Config + registry */
76
+ /* -------------------------------------------------------------------------- */
65
77
 
66
78
  const config: Required<GumPluginOptions> = {
67
79
  rememberPrefix: "gum",
68
80
  recentlySuccessfulDuration: 2000
69
81
  };
70
82
 
71
- /**
72
- * Why: preserveState=false should clear only state for one page.
73
- * When: called by remember registration and GET visits.
74
- * Where: used in this Gum plugin storage registry.
75
- */
76
83
  const routeRememberKeys = new Map<string, Set<string>>();
77
84
 
78
- /**
79
- * Why: route keys must be consistent across query changes.
80
- * When: before read/write in remember key registry.
81
- * Where: internal helper for Gum path-based state tracking.
82
- */
83
85
  function normalizePath(path: string) {
84
86
  const clean = (path || "/").split("?")[0];
85
87
  return clean || "/";
@@ -87,15 +89,10 @@ function normalizePath(path: string) {
87
89
 
88
90
  function registerRememberKey(path: string, key: string) {
89
91
  const routePath = normalizePath(path);
90
- if (!routeRememberKeys.has(routePath)) routeRememberKeys.set(routePath, new Set<string>());
91
- routeRememberKeys.get(routePath)?.add(key);
92
+ if (!routeRememberKeys.has(routePath)) routeRememberKeys.set(routePath, new Set());
93
+ routeRememberKeys.get(routePath)!.add(key);
92
94
  }
93
95
 
94
- /**
95
- * Why: emulate Inertia preserveState=false behavior.
96
- * When: a GET visit requests state reset.
97
- * Where: removes entries from localStorage using Gum prefix.
98
- */
99
96
  export function clearRememberForPath(path: string) {
100
97
  const routePath = normalizePath(path);
101
98
  const keys = routeRememberKeys.get(routePath);
@@ -103,38 +100,155 @@ export function clearRememberForPath(path: string) {
103
100
  keys.forEach((key) => localStorage.removeItem(`${config.rememberPrefix}:${key}`));
104
101
  }
105
102
 
103
+ /* -------------------------------------------------------------------------- */
104
+ /* Global processing tracker (shared across all useGum() instances) */
105
+ /* -------------------------------------------------------------------------- */
106
+
107
+ const globalProcessing = ref(false);
108
+ let activeRequests = 0;
109
+
110
+ function trackStart() {
111
+ activeRequests++;
112
+ globalProcessing.value = true;
113
+ }
114
+
115
+ function trackEnd() {
116
+ activeRequests = Math.max(0, activeRequests - 1);
117
+ if (activeRequests === 0) globalProcessing.value = false;
118
+ }
119
+
120
+ /* -------------------------------------------------------------------------- */
121
+ /* Small shared helpers */
122
+ /* -------------------------------------------------------------------------- */
123
+
124
+ export function normalizeValidationErrors(payload?: ValidationErrorPayload): NormalizedErrors {
125
+ const normalized: NormalizedErrors = { ...payload?.errors };
126
+ const issues = payload?.error?.issues;
127
+ if (Array.isArray(issues)) {
128
+ issues.forEach((issue) => {
129
+ const key = issue.path?.map(String).join(".");
130
+ if (key) normalized[key] = String(issue.message || "Invalid value");
131
+ });
132
+ }
133
+ return normalized;
134
+ }
135
+
136
+ async function runBefore(onBefore?: LifecycleHooks["onBefore"]) {
137
+ const allow = await onBefore?.();
138
+ return allow !== false;
139
+ }
140
+
141
+ async function restoreScroll(enabled: boolean, y: number) {
142
+ if (!enabled) return;
143
+ await nextTick();
144
+ // Double rAF: survives one layout pass (images/fonts shifting content).
145
+ requestAnimationFrame(() => requestAnimationFrame(() => window.scrollTo({ top: y, behavior: "instant" as ScrollBehavior })));
146
+ }
147
+
148
+ function startRecentTimer(target: { recentlySuccessful: { value: boolean } }) {
149
+ target.recentlySuccessful.value = true;
150
+ setTimeout(() => {
151
+ target.recentlySuccessful.value = false;
152
+ }, config.recentlySuccessfulDuration);
153
+ }
154
+
155
+ function assertSource(url: string | undefined, fetcher: Fetcher | undefined, ctx: string) {
156
+ if (!url && !fetcher) {
157
+ throw new Error(
158
+ `gum: ${ctx} requires either "url" or "fetcher". ` + `Pass a URL for axios, or a fetcher (e.g. a store method) to delegate the call.`
159
+ );
160
+ }
161
+ if (import.meta.env?.DEV && url && fetcher) {
162
+ console.warn(`gum: ${ctx} — "url" is ignored when "fetcher" is provided.`);
163
+ }
164
+ }
165
+
166
+ /** Read the current scroll position — plain read, no per-instance listeners. */
167
+ function readScrollY(): number {
168
+ return typeof window !== "undefined" ? window.scrollY : 0;
169
+ }
170
+
171
+ /* -------------------------------------------------------------------------- */
172
+ /* Shared core executor — dumb transport, never swallows errors */
173
+ /* -------------------------------------------------------------------------- */
174
+
175
+ type ExecuteOptions = {
176
+ method: GumMethod;
177
+ url?: string;
178
+ payload: Record<string, unknown> | FormData | undefined;
179
+ query?: Record<string, unknown>;
180
+ fetcher?: Fetcher;
181
+ onProgress?: (event: AxiosProgressEvent) => void;
182
+ };
183
+
106
184
  /**
107
- * Why: persist page-local UI state across navigations/reloads.
108
- * When: page needs remembered filters/forms.
109
- * Where: composables/pages calling useGumRemember.
185
+ * Execute the network call. Throws on error — the caller normalizes and
186
+ * decides whether to rethrow based on whether `onError` was provided.
187
+ *
188
+ * Fetcher results:
189
+ * - If it looks like an AxiosResponse (object with `data` and a numeric `status`),
190
+ * it is returned as-is.
191
+ * - Otherwise it is wrapped into `{ data: result, status: 200 }`.
110
192
  */
193
+ async function execute(options: ExecuteOptions): Promise<AxiosResponse> {
194
+ if (options.fetcher) {
195
+ const result = await options.fetcher(options.payload ?? {});
196
+ const looksLikeResponse =
197
+ result !== null &&
198
+ typeof result === "object" &&
199
+ "data" in (result as Record<string, unknown>) &&
200
+ typeof (result as { status?: unknown }).status === "number";
201
+ if (looksLikeResponse) return result as AxiosResponse;
202
+ return { data: result, status: 200, statusText: "OK", headers: {}, config: {} } as AxiosResponse;
203
+ }
204
+
205
+ return await axios.request({
206
+ method: options.method,
207
+ url: options.url,
208
+ params: options.query,
209
+ data: options.method === "get" ? undefined : options.payload,
210
+ onUploadProgress: options.onProgress
211
+ });
212
+ }
213
+
214
+ /* -------------------------------------------------------------------------- */
215
+ /* Remember */
216
+ /* -------------------------------------------------------------------------- */
217
+
111
218
  export function useGumRemember<T extends object>(key: string, initial: T) {
112
219
  const route = useRoute();
113
220
  registerRememberKey(route.path, key);
114
-
115
221
  return useStorage<T>(`${config.rememberPrefix}:${key}`, initial, localStorage, {
116
222
  mergeDefaults: true
117
223
  });
118
224
  }
119
225
 
120
- /**
121
- * Why: provide Inertia-style visit API for requests + navigation.
122
- * When: pages trigger get/post/put/patch/delete/reload flows.
123
- * Where: UI pages/composables that import useGum.
124
- */
125
- export function useGum() {
226
+ /* -------------------------------------------------------------------------- */
227
+ /* Visit API */
228
+ /* -------------------------------------------------------------------------- */
229
+
230
+ function buildVisitApi() {
126
231
  const router = useRouter();
127
232
  const route = useRoute();
128
- const processing = ref(false);
129
-
130
- /**
131
- * Why: unify request lifecycle hooks with router sync.
132
- * When: any Gum visit method is executed.
133
- * Where: internal core of useGum().
134
- */
135
- async function visit(url: string, options: GumVisitOptions = {}) {
233
+
234
+ /* ---------------------------- visit core ------------------------------- */
235
+
236
+ async function runVisit(params: {
237
+ method: GumMethod;
238
+ url?: string;
239
+ data?: Record<string, unknown> | FormData;
240
+ query?: Record<string, unknown>;
241
+ routePath?: string;
242
+ replace?: boolean;
243
+ preserveState?: boolean;
244
+ preserveScroll?: boolean;
245
+ skipFetch?: boolean;
246
+ fetcher?: Fetcher;
247
+ hooks?: GumVisitOptions;
248
+ }) {
136
249
  const {
137
- method = "get",
250
+ method,
251
+ url,
138
252
  data,
139
253
  query,
140
254
  routePath,
@@ -142,193 +256,354 @@ export function useGum() {
142
256
  preserveState = method !== "get",
143
257
  preserveScroll = false,
144
258
  skipFetch = false,
145
- onBefore,
146
- onStart,
147
- onProgress,
148
- onSuccess,
149
- onError,
150
- onFinish
151
- } = options;
259
+ fetcher,
260
+ hooks = {}
261
+ } = params;
152
262
 
153
- const allow = await onBefore?.();
154
- if (allow === false) return;
263
+ const { onBefore, onStart, onProgress, onSuccess, onError, onFinish } = hooks;
155
264
 
156
- const scrollY = window.scrollY;
157
- processing.value = true;
158
- await onStart?.();
265
+ if (!(await runBefore(onBefore))) return;
159
266
 
160
- let response: AxiosResponse | undefined;
267
+ const savedY = readScrollY();
268
+ trackStart();
161
269
 
270
+ let response: AxiosResponse | undefined;
162
271
  try {
272
+ // onStart is inside try so a throw still unwinds processing.
273
+ await onStart?.();
274
+
163
275
  if (!skipFetch) {
164
- response = await axios.request({
165
- method,
166
- url,
167
- params: method === "get" ? (query ?? (data as Record<string, unknown> | undefined)) : query,
168
- data: method === "get" ? undefined : data,
169
- onUploadProgress: (event) => onProgress?.(event)
170
- });
276
+ assertSource(url, fetcher, "visit");
277
+ try {
278
+ response = await execute({ method, url, payload: data, query, fetcher, onProgress });
279
+ } catch (error) {
280
+ const err = error as AxiosError<ValidationErrorPayload>;
281
+ const normalized = normalizeValidationErrors(err.response?.data);
282
+ await onError?.(normalized, err);
283
+ if (!onError) throw error;
284
+ return;
285
+ }
171
286
  }
172
287
 
173
288
  if (method === "get") {
174
- const targetRoutePath = routePath ?? route.path;
175
- if (!preserveState) clearRememberForPath(targetRoutePath);
176
-
177
- const payload = {
178
- path: targetRoutePath,
179
- query: (query ?? route.query) as LocationQueryRaw
180
- };
181
-
289
+ const targetPath = routePath ?? route.path;
290
+ if (!preserveState) clearRememberForPath(targetPath);
291
+ const payload = { path: targetPath, query: (query ?? route.query) as LocationQueryRaw };
182
292
  if (replace) await router.replace(payload);
183
293
  else await router.push(payload);
184
294
  }
185
295
 
186
296
  await onSuccess?.(response!);
187
297
  return response;
188
- } catch (error) {
189
- const err = error as AxiosError<ValidationErrorPayload>;
190
- const handled = !!options.onError;
191
- await onError?.(normalizeValidationErrors(err.response?.data), error as AxiosError);
192
- if (!handled) throw error;
193
298
  } finally {
194
- processing.value = false;
299
+ trackEnd();
195
300
  await onFinish?.();
196
- if (preserveScroll) {
197
- await nextTick();
198
- requestAnimationFrame(() => window.scrollTo({ top: scrollY }));
199
- }
301
+ await restoreScroll(preserveScroll, savedY);
200
302
  }
201
303
  }
202
304
 
203
- return {
204
- processing,
305
+ function visit(url: string, options?: GumVisitOptions): Promise<AxiosResponse | undefined>;
306
+ function visit(options: GumVisitOptions & { fetcher: Fetcher }): Promise<AxiosResponse | undefined>;
307
+ function visit(urlOrOptions: string | (GumVisitOptions & { fetcher: Fetcher }), options: GumVisitOptions = {}) {
308
+ if (typeof urlOrOptions === "object") {
309
+ return runVisit({
310
+ method: urlOrOptions.method ?? "get",
311
+ fetcher: urlOrOptions.fetcher,
312
+ data: urlOrOptions.data,
313
+ query: urlOrOptions.query,
314
+ routePath: urlOrOptions.routePath,
315
+ replace: urlOrOptions.replace,
316
+ preserveState: urlOrOptions.preserveState,
317
+ preserveScroll: urlOrOptions.preserveScroll,
318
+ skipFetch: urlOrOptions.skipFetch,
319
+ hooks: urlOrOptions
320
+ });
321
+ }
322
+ return runVisit({
323
+ method: options.method ?? "get",
324
+ url: urlOrOptions,
325
+ data: options.data,
326
+ query: options.query,
327
+ routePath: options.routePath,
328
+ replace: options.replace,
329
+ preserveState: options.preserveState,
330
+ preserveScroll: options.preserveScroll,
331
+ skipFetch: options.skipFetch,
332
+ fetcher: options.fetcher,
333
+ hooks: options
334
+ });
335
+ }
336
+
337
+ const api = {
338
+ processing: globalProcessing,
205
339
  visit,
206
- get: (url: string, options: Omit<GumVisitOptions, "method"> = {}) => visit(url, { ...options, method: "get" }),
207
- post: (url: string, data?: Record<string, unknown> | FormData, options: Omit<GumVisitOptions, "method" | "data"> = {}) => {
208
- return visit(url, { ...options, method: "post", data });
209
- },
210
- put: (url: string, data?: Record<string, unknown> | FormData, options: Omit<GumVisitOptions, "method" | "data"> = {}) => {
211
- return visit(url, { ...options, method: "put", data });
212
- },
213
- patch: (url: string, data?: Record<string, unknown> | FormData, options: Omit<GumVisitOptions, "method" | "data"> = {}) => {
214
- return visit(url, { ...options, method: "patch", data });
215
- },
216
- delete: (url: string, options: Omit<GumVisitOptions, "method"> = {}) => visit(url, { ...options, method: "delete" }),
217
- reload: (options: Omit<GumVisitOptions, "method"> = {}) => {
218
- return visit(route.path, {
219
- ...options,
340
+ get: (url: string, o: GumVisitOptions = {}) =>
341
+ runVisit({
342
+ method: "get",
343
+ url,
344
+ query: o.query,
345
+ routePath: o.routePath,
346
+ replace: o.replace,
347
+ preserveState: o.preserveState,
348
+ preserveScroll: o.preserveScroll,
349
+ skipFetch: o.skipFetch,
350
+ fetcher: o.fetcher,
351
+ hooks: o
352
+ }),
353
+ post: (url: string, data?: GumVisitOptions["data"], o: GumVisitOptions = {}) =>
354
+ runVisit({
355
+ method: "post",
356
+ url,
357
+ data,
358
+ query: o.query,
359
+ routePath: o.routePath,
360
+ replace: o.replace,
361
+ preserveState: o.preserveState,
362
+ preserveScroll: o.preserveScroll,
363
+ skipFetch: o.skipFetch,
364
+ fetcher: o.fetcher,
365
+ hooks: o
366
+ }),
367
+ put: (url: string, data?: GumVisitOptions["data"], o: GumVisitOptions = {}) =>
368
+ runVisit({
369
+ method: "put",
370
+ url,
371
+ data,
372
+ query: o.query,
373
+ routePath: o.routePath,
374
+ replace: o.replace,
375
+ preserveState: o.preserveState,
376
+ preserveScroll: o.preserveScroll,
377
+ skipFetch: o.skipFetch,
378
+ fetcher: o.fetcher,
379
+ hooks: o
380
+ }),
381
+ patch: (url: string, data?: GumVisitOptions["data"], o: GumVisitOptions = {}) =>
382
+ runVisit({
383
+ method: "patch",
384
+ url,
385
+ data,
386
+ query: o.query,
387
+ routePath: o.routePath,
388
+ replace: o.replace,
389
+ preserveState: o.preserveState,
390
+ preserveScroll: o.preserveScroll,
391
+ skipFetch: o.skipFetch,
392
+ fetcher: o.fetcher,
393
+ hooks: o
394
+ }),
395
+ delete: (url: string, o: GumVisitOptions = {}) =>
396
+ // Forward o.data so DELETE bodies keep working (legacy behavior).
397
+ runVisit({
398
+ method: "delete",
399
+ url,
400
+ data: o.data,
401
+ query: o.query,
402
+ routePath: o.routePath,
403
+ replace: o.replace,
404
+ preserveState: o.preserveState,
405
+ preserveScroll: o.preserveScroll,
406
+ skipFetch: o.skipFetch,
407
+ fetcher: o.fetcher,
408
+ hooks: o
409
+ }),
410
+ reload: (o: GumVisitOptions = {}) =>
411
+ runVisit({
220
412
  method: "get",
413
+ url: route.path,
414
+ query: route.query as Record<string, unknown>,
221
415
  replace: true,
222
- query: route.query as Record<string, unknown>
223
- });
224
- }
416
+ preserveState: o.preserveState,
417
+ preserveScroll: o.preserveScroll,
418
+ skipFetch: o.skipFetch,
419
+ fetcher: o.fetcher,
420
+ hooks: o
421
+ }),
422
+
423
+ /* URL-less, fetcher-based variants */
424
+ getWith: (o: VisitWithFetcherOptions): Promise<AxiosResponse | undefined> =>
425
+ runVisit({
426
+ method: "get",
427
+ query: o.query,
428
+ routePath: o.routePath,
429
+ replace: o.replace,
430
+ preserveState: o.preserveState,
431
+ preserveScroll: o.preserveScroll,
432
+ skipFetch: o.skipFetch,
433
+ fetcher: o.fetcher,
434
+ hooks: o
435
+ }),
436
+ postWith: (o: VisitWithFetcherDataOptions): Promise<AxiosResponse | undefined> =>
437
+ runVisit({
438
+ method: "post",
439
+ data: o.data,
440
+ query: o.query,
441
+ routePath: o.routePath,
442
+ replace: o.replace,
443
+ preserveState: o.preserveState,
444
+ preserveScroll: o.preserveScroll,
445
+ skipFetch: o.skipFetch,
446
+ fetcher: o.fetcher,
447
+ hooks: o
448
+ }),
449
+ putWith: (o: VisitWithFetcherDataOptions): Promise<AxiosResponse | undefined> =>
450
+ runVisit({
451
+ method: "put",
452
+ data: o.data,
453
+ query: o.query,
454
+ routePath: o.routePath,
455
+ replace: o.replace,
456
+ preserveState: o.preserveState,
457
+ preserveScroll: o.preserveScroll,
458
+ skipFetch: o.skipFetch,
459
+ fetcher: o.fetcher,
460
+ hooks: o
461
+ }),
462
+ patchWith: (o: VisitWithFetcherDataOptions): Promise<AxiosResponse | undefined> =>
463
+ runVisit({
464
+ method: "patch",
465
+ data: o.data,
466
+ query: o.query,
467
+ routePath: o.routePath,
468
+ replace: o.replace,
469
+ preserveState: o.preserveState,
470
+ preserveScroll: o.preserveScroll,
471
+ skipFetch: o.skipFetch,
472
+ fetcher: o.fetcher,
473
+ hooks: o
474
+ }),
475
+ deleteWith: (o: VisitWithFetcherDataOptions): Promise<AxiosResponse | undefined> =>
476
+ runVisit({
477
+ method: "delete",
478
+ data: o.data,
479
+ query: o.query,
480
+ routePath: o.routePath,
481
+ replace: o.replace,
482
+ preserveState: o.preserveState,
483
+ preserveScroll: o.preserveScroll,
484
+ skipFetch: o.skipFetch,
485
+ fetcher: o.fetcher,
486
+ hooks: o
487
+ }),
488
+ reloadWith: (o: VisitWithFetcherOptions): Promise<AxiosResponse | undefined> =>
489
+ runVisit({
490
+ method: "get",
491
+ query: (o.query ?? route.query) as Record<string, unknown>,
492
+ routePath: o.routePath,
493
+ replace: true,
494
+ preserveState: o.preserveState,
495
+ preserveScroll: o.preserveScroll,
496
+ skipFetch: o.skipFetch,
497
+ fetcher: o.fetcher,
498
+ hooks: o
499
+ })
225
500
  };
501
+
502
+ return api;
226
503
  }
227
504
 
228
- /**
229
- * Why: centralize form state/errors/progress like Inertia useForm.
230
- * When: create/update/delete forms submit to backend.
231
- * Where: UI forms using useGumForm.
232
- */
233
- export function useGumForm<T extends Record<string, unknown>>(defaults: T) {
505
+ type GumApi = ReturnType<typeof buildVisitApi>;
506
+
507
+ /* -------------------------------------------------------------------------- */
508
+ /* Form state */
509
+ /* -------------------------------------------------------------------------- */
510
+
511
+ function createForm<T extends Record<string, unknown>>(defaults: T) {
512
+ /* ------------------------ form state ----------------------------------- */
234
513
  const initial = structuredClone(defaults);
235
514
  const data = reactive(structuredClone(defaults)) as T;
236
515
  const errors = reactive<Record<string, string>>({});
237
516
  const progress = ref<number | null>(null);
238
- const processing = ref(false);
517
+ const formProcessing = ref(false);
239
518
  const wasSuccessful = ref(false);
240
519
  const recentlySuccessful = ref(false);
241
520
  const isDirty = computed(() => JSON.stringify(toRaw(data)) !== JSON.stringify(initial));
242
521
 
243
- /**
244
- * Why: allow manual error assignment for custom validations.
245
- * When: setting one field error outside server response.
246
- * Where: useGumForm consumer code.
247
- */
248
522
  function setError(field: keyof T | string, message: string) {
249
523
  errors[String(field)] = message;
250
524
  }
251
525
 
252
- /**
253
- * Why: keep error state in sync with user actions/submits.
254
- * When: before submit or after field corrections.
255
- * Where: useGumForm internal + consumer calls.
256
- */
257
526
  function clearErrors(...fields: (keyof T | string)[]) {
258
527
  if (!fields.length) {
259
- Object.keys(errors).forEach((key) => delete errors[key]);
528
+ Object.keys(errors).forEach((k) => delete errors[k]);
260
529
  return;
261
530
  }
262
- fields.forEach((field) => delete errors[String(field)]);
531
+ fields.forEach((f) => delete errors[String(f)]);
263
532
  }
264
533
 
265
- /**
266
- * Why: restore form values to initial defaults safely.
267
- * When: cancel edit or after successful submission.
268
- * Where: useGumForm consumer actions.
269
- */
270
534
  function reset(...fields: (keyof T)[]) {
271
535
  if (!fields.length) {
272
536
  Object.assign(data, structuredClone(initial));
273
537
  clearErrors();
274
538
  return;
275
539
  }
276
-
277
- fields.forEach((field) => {
278
- data[field] = structuredClone(initial[field]);
279
- delete errors[String(field)];
540
+ fields.forEach((f) => {
541
+ (data as any)[f] = structuredClone(initial[f]);
542
+ delete errors[String(f)];
280
543
  });
281
544
  }
282
545
 
283
- /**
284
- * Why: provide a single submission path with lifecycle hooks.
285
- * When: post/put/patch/delete helpers are called.
286
- * Where: useGumForm internal request executor.
287
- */
288
- async function submit(method: FormMethod, url: string, payload?: Record<string, unknown>, options: FormSubmitOptions = {}) {
289
- const { onBefore, onStart, onProgress, onSuccess, onError, onFinish } = options;
546
+ async function submitForm(
547
+ method: FormMethod,
548
+ url: string | undefined,
549
+ payload: Record<string, unknown> | undefined,
550
+ opts: FormSubmitOptions<T>
551
+ ): Promise<AxiosResponse | undefined> {
552
+ const { onBefore, onStart, onProgress, onSuccess, onError, onFinish, validate, transform, fetcher } = opts;
553
+ const userOnError = onError;
554
+
555
+ if (!(await runBefore(onBefore))) return;
556
+
557
+ assertSource(url, fetcher, "submit");
290
558
 
291
- const allow = await onBefore?.();
292
- if (allow === false) return;
559
+ // Local validation — only abort if there ARE errors (empty {} passes through).
560
+ const validationErrors = validate?.(toRaw(data) as T);
561
+ if (validationErrors && Object.keys(validationErrors).length > 0) {
562
+ clearErrors();
563
+ Object.entries(validationErrors).forEach(([k, v]) => setError(k, v));
564
+ return;
565
+ }
566
+
567
+ const body = transform ? transform(toRaw(data) as T) : (payload ?? toRaw(data));
293
568
 
294
569
  wasSuccessful.value = false;
295
570
  clearErrors();
296
- processing.value = true;
297
- await onStart?.();
571
+ formProcessing.value = true;
572
+ trackStart();
298
573
 
299
574
  try {
300
- const response = await axios.request({
301
- method,
302
- url,
303
- data: payload ?? toRaw(data),
304
- onUploadProgress: (event) => {
305
- onProgress?.(event);
306
- if (!event.total) return;
307
- progress.value = Math.round((event.loaded * 100) / event.total);
308
- }
309
- });
575
+ // onStart inside try — a throw still unwinds processing.
576
+ await onStart?.();
577
+
578
+ let response: AxiosResponse;
579
+ try {
580
+ response = await execute({
581
+ method,
582
+ url,
583
+ payload: body,
584
+ fetcher,
585
+ onProgress: (event) => {
586
+ onProgress?.(event);
587
+ if (event.total) progress.value = Math.round((event.loaded * 100) / event.total);
588
+ }
589
+ });
590
+ } catch (error) {
591
+ const err = error as AxiosError<ValidationErrorPayload>;
592
+ const normalized = normalizeValidationErrors(err.response?.data);
593
+ Object.entries(normalized).forEach(([k, v]) => setError(k, Array.isArray(v) ? v[0] : v));
594
+ await userOnError?.(normalized, err);
595
+ if (!userOnError) throw error;
596
+ return;
597
+ }
310
598
 
311
599
  wasSuccessful.value = true;
312
- recentlySuccessful.value = true;
313
- setTimeout(() => {
314
- recentlySuccessful.value = false;
315
- }, config.recentlySuccessfulDuration);
316
- await onSuccess?.(response!);
600
+ startRecentTimer({ recentlySuccessful });
601
+ await onSuccess?.(response);
317
602
  return response;
318
- } catch (error) {
319
- const err = error as AxiosError<ValidationErrorPayload>;
320
- const normalizedErrors = normalizeValidationErrors(err.response?.data);
321
-
322
- Object.entries(normalizedErrors).forEach(([key, value]) => {
323
- errors[String(key)] = Array.isArray(value) ? value[0] : value;
324
- });
325
-
326
- const handled = !!options.onError;
327
- await onError?.(normalizedErrors, error as AxiosError);
328
- if (!handled) throw error;
329
603
  } finally {
330
- processing.value = false;
604
+ formProcessing.value = false;
331
605
  progress.value = null;
606
+ trackEnd();
332
607
  await onFinish?.();
333
608
  }
334
609
  }
@@ -337,26 +612,67 @@ export function useGumForm<T extends Record<string, unknown>>(defaults: T) {
337
612
  data,
338
613
  errors: errors as FormErrors<T>,
339
614
  progress,
340
- processing,
615
+ processing: formProcessing,
341
616
  wasSuccessful,
342
617
  recentlySuccessful,
343
618
  isDirty,
344
619
  setError,
345
620
  clearErrors,
346
621
  reset,
347
- submit,
348
- post: (url: string, payload?: Record<string, unknown>, options?: FormSubmitOptions) => submit("post", url, payload, options),
349
- put: (url: string, payload?: Record<string, unknown>, options?: FormSubmitOptions) => submit("put", url, payload, options),
350
- patch: (url: string, payload?: Record<string, unknown>, options?: FormSubmitOptions) => submit("patch", url, payload, options),
351
- delete: (url: string, payload?: Record<string, unknown>, options?: FormSubmitOptions) => submit("delete", url, payload, options)
622
+ submit: (method: FormMethod, url: string | undefined, payload?: Record<string, unknown>, opts?: FormSubmitOptions<T>) =>
623
+ submitForm(method, url, payload, opts ?? {}),
624
+ post: (url: string, payload?: Record<string, unknown>, opts?: FormSubmitOptions<T>) => submitForm("post", url, payload, opts ?? {}),
625
+ put: (url: string, payload?: Record<string, unknown>, opts?: FormSubmitOptions<T>) => submitForm("put", url, payload, opts ?? {}),
626
+ patch: (url: string, payload?: Record<string, unknown>, opts?: FormSubmitOptions<T>) => submitForm("patch", url, payload, opts ?? {}),
627
+ delete: (url: string, payload?: Record<string, unknown>, opts?: FormSubmitOptions<T>) => submitForm("delete", url, payload, opts ?? {}),
628
+
629
+ /* URL-less, fetcher-based variants */
630
+ postWith: (o: FormWithFetcherOptions<T>) => submitForm("post", undefined, undefined, o),
631
+ putWith: (o: FormWithFetcherOptions<T>) => submitForm("put", undefined, undefined, o),
632
+ patchWith: (o: FormWithFetcherOptions<T>) => submitForm("patch", undefined, undefined, o),
633
+ deleteWith: (o: FormWithFetcherOptions<T>) => submitForm("delete", undefined, undefined, o)
352
634
  };
353
635
  }
354
636
 
637
+ type GumForm<T extends Record<string, unknown>> = ReturnType<typeof createForm<T>>;
638
+
639
+ /* -------------------------------------------------------------------------- */
640
+ /* useGum — unified visit + form */
641
+ /* -------------------------------------------------------------------------- */
642
+
643
+ /**
644
+ * Visit API only. `form` is not available when no defaults are passed.
645
+ */
646
+ export function useGum(): GumApi & { form?: undefined };
647
+
648
+ /**
649
+ * Visit API plus form state. Pass the form defaults as the first argument.
650
+ */
651
+ export function useGum<T extends Record<string, unknown>>(defaults: T): GumApi & { form: GumForm<T> };
652
+
653
+ export function useGum<T extends Record<string, unknown>>(defaults?: T): GumApi & { form?: GumForm<T> } {
654
+ const api = buildVisitApi();
655
+ if (defaults === undefined) return api;
656
+ return { ...api, form: createForm(defaults) };
657
+ }
658
+
659
+ /* -------------------------------------------------------------------------- */
660
+ /* Backward-compat alias */
661
+ /* -------------------------------------------------------------------------- */
662
+
355
663
  /**
356
- * Why: configure shared Gum behavior globally.
357
- * When: app bootstrap calls app.use(GumPlugin, options).
358
- * Where: src/resources/src/main.ts plugin registration.
664
+ * @deprecated Use `useGum(defaults).form` instead.
665
+ * Kept so existing pages don't break. Will be removed in a future major.
359
666
  */
667
+ export function useGumForm<T extends Record<string, unknown>>(defaults: T): GumForm<T> {
668
+ const { form } = useGum(defaults);
669
+ return form;
670
+ }
671
+
672
+ /* -------------------------------------------------------------------------- */
673
+ /* Plugin */
674
+ /* -------------------------------------------------------------------------- */
675
+
360
676
  export const GumPlugin = {
361
677
  install(_app: App, options: GumPluginOptions = {}) {
362
678
  if (options.rememberPrefix) config.rememberPrefix = options.rememberPrefix;